http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/AppxManifest.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/AppxManifest.js b/template/cordova/lib/AppxManifest.js new file mode 100644 index 0000000..461ac64 --- /dev/null +++ b/template/cordova/lib/AppxManifest.js @@ -0,0 +1,595 @@ +/** + 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 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 + * @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 + * @return {AppxManifest|Win10AppxManifest} Manifest instance + */ +AppxManifest.get = function (fileName) { + + if (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; + return (manifestCache[fileName] = new Manifest(fileName, prefix)); +}; + +AppxManifest.prototype.getPhoneIdentity = function () { + var phoneIdentity = this.doc.getroot().find('./mp:PhoneIdentity'); + if (!phoneIdentity) + throw new Error('Failed to find PhoneIdentity element.'); + + return { + getPhoneProductId: function () { + return phoneIdentity.attrib.PhoneProductId; + }, + setPhoneProductId: function (id) { + if (!id) throw new Error('Argument for "setPhoneProductId" must be defined'); + 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'); + 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'); + 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'); + + // 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'); + 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'); + var publisher = properties.find('./PublisherDisplayName'); + + if (!publisher) { + publisher = new et.Element('PublisherDisplayName'); + properties.append(publisher); + } + + publisher.text = name; + + 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 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'); + // 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 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'); + 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'); + + visualElements.attrib.BackgroundColor = refineColor(color); + return this; + }, + trySetBackgroundColor: function (color) { + try { + return this.setBackgroundColor(color); + } catch (e) { 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 (color && splashNode) { + splashNode.attrib.BackgroundColor = refineColor(color); + } else if (!color) { + delete splashNode.attrib.BackgroundColor; + } + 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; + } + }; +}; + +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 refineColor(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; +} + +// 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'); + 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; }; + + 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) { + ensureUapPrefixedCapabilities(this.doc.find('.//Capabilities')); + // 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'); +}; + +/** + * 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; + } + }); +} + +module.exports = AppxManifest;
http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/ConfigParser.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/ConfigParser.js b/template/cordova/lib/ConfigParser.js index 0fe4b4c..c9ce6a8 100644 --- a/template/cordova/lib/ConfigParser.js +++ b/template/cordova/lib/ConfigParser.js @@ -17,213 +17,167 @@ under the License. */ -/* jshint node:true, bitwise:true, undef:true, trailing:true, quotmark:true, - indent:4, unused:vars, latedef:nofunc, - sub:true -*/ +/* jshint sub:true */ -var et = require('elementtree'), - fs = require('fs'); - -/** Wraps a config.xml file */ -function ConfigParser(path) { - this.path = path; - try { - var contents = fs.readFileSync(path, 'utf-8'); - if(contents) { - //Windows is the BOM. Skip the Byte Order Mark. - contents = contents.substring(contents.indexOf('<')); - } - this.doc = new et.ElementTree(et.XML(contents)); +var util = require('util'); +var Version = require('./Version'); +var ConfigParser = require('cordova-common').ConfigParser; - } catch (e) { - console.error('Parsing '+path+' failed'); - throw e; - } - var r = this.doc.getroot(); - if (r.tag !== 'widget') { - throw new Error(path + ' has incorrect root node name (expected "widget", was "' + r.tag + '")'); - } -} +var BASE_UAP_VERSION = new Version(10, 0, 10240, 0); -function getNodeTextSafe(el) { - return el && el.text && el.text.trim(); +/** + * A wrapper arount config.xml file, based on cordova-common implementation, + * extended with some windows-specific methods. + * + * @constructor + * @extends {ConfigParser} + * + * @param {String} path Path to config.xml file + */ +function WindowsConfigParser(path) { + ConfigParser.call(this, path); } -function findOrCreate(doc, name) { - var ret = doc.find(name); - if (!ret) { - ret = new et.Element(name); - doc.getroot().append(ret); +util.inherits(WindowsConfigParser, ConfigParser); + +WindowsConfigParser.prototype.startPage = function() { + var content = this.doc.find('content'); + if (content) { + return content.attrib.src; } - return ret; -} + return null; +}; + +WindowsConfigParser.prototype.windows_packageVersion = function() { + return this.doc.getroot().attrib['windows-packageVersion']; +}; -ConfigParser.prototype = { - packageName: function(id) { - return this.doc.getroot().attrib['id']; - }, - setPackageName: function(id) { - this.doc.getroot().attrib['id'] = id; - }, - name: function() { - return getNodeTextSafe(this.doc.find('name')); - }, - setName: function(name) { - var el = findOrCreate(this.doc, 'name'); - el.text = name; - }, - startPage: function() { - var content = this.doc.find('content'); - if (content) { - return content.attrib.src; +WindowsConfigParser.prototype.getMatchingPreferences = function(regexp) { + var preferences = this.doc.findall('preference'); + var result = []; + preferences.forEach(function(preference) { + if (regexp.test(preference.attrib.name)) { + result.push({ name: preference.attrib.name, value: preference.attrib.value }); } - return null; - }, - description: function() { - return this.doc.find('description').text.trim(); - }, - setDescription: function(text) { - var el = findOrCreate(this.doc, 'description'); - el.text = text; - }, - version: function() { - return this.doc.getroot().attrib['version']; - }, - windows_packageVersion: function() { - return this.doc.getroot().attrib['windows-packageVersion']; - }, - android_versionCode: function() { - return this.doc.getroot().attrib['android-versionCode']; - }, - ios_CFBundleVersion: function() { - return this.doc.getroot().attrib['ios-CFBundleVersion']; - }, - setVersion: function(value) { - this.doc.getroot().attrib['version'] = value; - }, - author: function() { - return getNodeTextSafe(this.doc.find('author')); - }, - getPreference: function(name) { - var preferences = this.doc.findall('preference'); - var ret = null; - preferences.forEach(function (preference) { - // Take the last one that matches. - if (preference.attrib.name.toLowerCase() === name.toLowerCase()) { - ret = preference.attrib.value; - } - }); - return ret; - }, - getMatchingPreferences: function(regexp) { - var preferences = this.doc.findall('preference'); - var result = []; - preferences.forEach(function(preference) { - if (regexp.test(preference.attrib.name)) { - result.push({ name: preference.attrib.name, value: preference.attrib.value }); - } - }); + }); - return result; - }, - /** - * Returns all resources. - * @param {string} resourceName Type of static resources to return. - * "icon" and "splash" currently supported. - * @return {Array} Resources for the platform specified. - */ - getStaticResources: function(resourceName) { - return this.doc.findall(resourceName).map(function (elt) { - var res = {}; - res.src = elt.attrib.src; - res.target = elt.attrib.target; - res.density = elt.attrib['density'] || elt.attrib['cdv:density'] || elt.attrib['gap:density']; - res.platform = elt.platform || null; // null means icon represents default icon (shared between platforms) - res.width = elt.attrib.width; - res.height = elt.attrib.height; - - return res; - }); - }, - - /** - * Returns all defined icons. - * @return {Resource[]} Array of icon objects. - */ - getIcons: function() { - return this.getStaticResources('icon'); - }, - - /** - * Returns all defined splash images. - * @return {Resource[]} Array of Splash objects. - */ - getSplashScreens: function() { - return this.getStaticResources('splash'); - }, - - /** - * Returns all access rules. - * @return {string[]} Array of access rules. - */ - getAccessRules: function() { - var rules = this.doc.getroot().findall('access'); - var ret = []; - rules.forEach(function (rule) { - if (rule.attrib.origin) { - ret.push(rule.attrib.origin); - } - }); - return ret; - }, - - /** - * Returns all <allow-navigation> rules. - * @return {string[]} Array of allow-navigation rules. - */ - getNavigationWhitelistRules: function() { - var rules = this.doc.getroot().findall('allow-navigation'); - var result = []; - rules.forEach(function(rule) { - if (rule.attrib.href) { - result.push(rule.attrib.href); - } - }); + return result; +}; + +WindowsConfigParser.prototype.getWindowsTargetVersion = function() { + var preference = this.getPreference('windows-target-version'); - return result; - }, + if (!preference) + preference = '8.1'; // default is 8.1. - getWindowsTargetVersion: function() { - var preference = this.getPreference('windows-target-version'); + return preference; +}; + +WindowsConfigParser.prototype.getWindowsPhoneTargetVersion = function() { + // This is a little more complicated than the previous one. + // 1. Check for an explicit preference. If the preference is set explicitly, return that, irrespective of whether it is valid + // 2. Get the Windows baseline version. If it's equivalent to 8.0, bump it to 8.1. + // 3. Return the Windows baseline version. + var explicitPreference = this.getPreference('windows-phone-target-version'); + if (explicitPreference) + return explicitPreference; - if (!preference) - preference = '8.1'; // default is 8.1. + var windowsTargetVersion = this.getWindowsTargetVersion(); + if (windowsTargetVersion === '8' || windowsTargetVersion === '8.0') + windowsTargetVersion = '8.1'; - return preference; - }, + return windowsTargetVersion; +}; + +/** + * Gets min/max UAP versions from the configuration. If no version preferences + * are in the configuration file, this will provide Windows.Universal at + * BASE_UAP_VERSION for both min and max. This will always return a rational + * object or will fail; for example, if a platform expects a higher + * min-version than max-version, it will raise the max version to the min + * version. + * + * @return {Object[]} An array of objects in the shape of: + * [ {'Name': 'Windows.Mobile', 'MinVersion': Version, 'MaxVersion': Version } ] (where + * Version is a Version object) + * + * @exception {RangeError} Thrown if a Version string is badly formed. + */ +WindowsConfigParser.prototype.getAllMinMaxUAPVersions = function () { + var uapVersionPreferenceTest = /(Microsoft.+?|Windows.+?)\-(MinVersion|MaxVersionTested)/i; + var platformBag = Object.create(null); + + this.getMatchingPreferences(uapVersionPreferenceTest) + .forEach(function(verPref) { + var matches = uapVersionPreferenceTest.exec(verPref.name); + // 'matches' should look like: ['Windows.Universal-MinVersion', 'Windows.Universal', 'MinVersion'] + var platformName = matches[1]; + var versionPropertyName = matches[2]; + + var platformVersionSet = platformBag[platformName]; + if (typeof platformVersionSet === 'undefined') { + platformVersionSet = { }; + platformBag[platformName] = platformVersionSet; + } - getWindowsPhoneTargetVersion: function() { - // This is a little more complicated than the previous one. - // 1. Check for an explicit preference. If the preference is set explicitly, return that, irrespective of whether it is valid - // 2. Get the Windows baseline version. If it's equivalent to 8.0, bump it to 8.1. - // 3. Return the Windows baseline version. - var explicitPreference = this.getPreference('windows-phone-target-version'); - if (explicitPreference) - return explicitPreference; + var versionTest = Version.tryParse(verPref.value); + if (!versionTest) { + throw new RangeError('Could not comprehend a valid version from the string "' + verPref.value + '" of platform-boundary "' + verPref.name + '".'); + } - var windowsTargetVersion = this.getWindowsTargetVersion(); - if (windowsTargetVersion === '8' || windowsTargetVersion === '8.0') - windowsTargetVersion = '8.1'; + platformVersionSet[versionPropertyName] = versionTest; + }); - return windowsTargetVersion; + for (var platformName in platformBag) { + // Go through each and make sure there are min/max set + var versionPref = platformBag[platformName]; + if (!versionPref.MaxVersionTested && !!versionPref.MinVersion) { // min is set, but max is not + versionPref.MaxVersionTested = versionPref.MinVersion; + } + else if (!versionPref.MinVersion && !!versionPref.MaxVersionTested) { // max is set, min is not + versionPref.MinVersion = versionPref.MaxVersionTested; + } + else if (!versionPref.MinVersion && !versionPref.MaxVersionTested) { // neither are set + versionPref.MinVersion = BASE_UAP_VERSION; + versionPref.MaxVersionTested = BASE_UAP_VERSION; + } + else { // both are set + if (versionPref.MinVersion.gt(versionPref.MaxVersionTested)) { + versionPref.MaxVersionTested = versionPref.MinVersion; + } + } + } - }, - - // Returns the widget defaultLocale - defaultLocale: function() { - return this.doc.getroot().attrib['defaultlocale']; + if (Object.keys(platformBag).length === 0) { + platformBag['Windows.Universal'] = { MinVersion: BASE_UAP_VERSION, MaxVersionTested: BASE_UAP_VERSION }; } + + return Object.keys(platformBag).map(function (platformName) { + return { + Name: platformName, + MinVersion: platformBag[platformName].MinVersion.toString(), + MaxVersionTested: platformBag[platformName].MaxVersionTested.toString(), + }; + }); +}; + +// Returns the widget defaultLocale +WindowsConfigParser.prototype.defaultLocale = function() { + return this.doc.getroot().attrib['defaultlocale']; +}; + +/** + * Checks to see whether access rules or + * @return {boolean} True if the config specifies remote URIs for access or start; false otherwise. + */ +WindowsConfigParser.prototype.hasRemoteUris = function() { + var test = /(https?|ms-appx-web):\/\//i; + + return test.test(this.startPage) || + this.getAllowNavigations() + .some(function(rule) { + return test.test(rule.href); + }); }; -module.exports = ConfigParser; +module.exports = WindowsConfigParser; http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/ConsoleLogger.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/ConsoleLogger.js b/template/cordova/lib/ConsoleLogger.js new file mode 100644 index 0000000..4ad6a75 --- /dev/null +++ b/template/cordova/lib/ConsoleLogger.js @@ -0,0 +1,75 @@ +/** + 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 loggerInstance; +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var CordovaError = require('cordova-common').CordovaError; + +/** + * @class ConsoleLogger + * @extends EventEmitter + * + * Implements basic logging for platform. Inherits regular NodeJS EventEmitter. + * All events, emitted on this class instance are immediately logged to + * console. + * + * Also attaches handler to process' uncaught exceptions, so these exceptions + * logged to console similar to regular error events. + */ +function ConsoleLogger() { + EventEmitter.call(this); + + var isVerbose = process.argv.indexOf('-d') >= 0 || process.argv.indexOf('--verbose') >= 0; + // For CordovaError print only the message without stack trace unless we + // are in a verbose mode. + process.on('uncaughtException', function(err){ + if ((err instanceof CordovaError) && isVerbose) { + console.error(err.stack); + } else { + console.error(err.message); + } + process.exit(1); + }); + + this.on('results', console.log); + this.on('verbose', function () { + if (isVerbose) + console.log.apply(console, arguments); + }); + this.on('info', console.log); + this.on('log', console.log); + this.on('warn', console.warn); +} +util.inherits(ConsoleLogger, EventEmitter); + +/** + * Returns already instantiated/newly created instance of ConsoleLogger class. + * This method should be used instead of creating ConsoleLogger directly, + * otherwise we'll get multiple handlers attached to process' + * uncaughtException + * + * @return {ConsoleLogger} New or already created instance of ConsoleLogger + */ +ConsoleLogger.get = function () { + loggerInstance = loggerInstance || new ConsoleLogger(); + return loggerInstance; +}; + +module.exports = ConsoleLogger; http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/JsprojManager.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/JsprojManager.js b/template/cordova/lib/JsprojManager.js new file mode 100644 index 0000000..714c7eb --- /dev/null +++ b/template/cordova/lib/JsprojManager.js @@ -0,0 +1,608 @@ +/** + 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 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)')); + } + return new jsprojManager(path.normalize(projectFiles[0])); +}; + +jsprojManager.prototype = { + _projects: null, + + getPackageName: function() { + return AppxManifest.get(path.join(this.root, 'package.windows.appxmanifest')) + .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 (relPath, targetConditions) { + events.emit('verbose', 'jsprojManager.addResourceFile(relPath: ' + relPath + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + // add hint path with full path + var link = new et.Element('Link'); + link.text = relPath; + var children = [link]; + + var copyToOutputDirectory = new et.Element('CopyToOutputDirectory'); + copyToOutputDirectory.text = 'Always'; + children.push(copyToOutputDirectory); + + var item = createItemGroupElement('ItemGroup/Content', relPath, 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) { + 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); + } + + 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(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"); + } + + 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(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"); + } + + 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; + } +}; + +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-windows/blob/58047a3d/template/cordova/lib/MSBuildTools.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/MSBuildTools.js b/template/cordova/lib/MSBuildTools.js index 1742bfa..c56c99a 100644 --- a/template/cordova/lib/MSBuildTools.js +++ b/template/cordova/lib/MSBuildTools.js @@ -17,12 +17,12 @@ under the License. */ -var Q = require('q'), - path = require('path'), - exec = require('./exec'), - shell = require('shelljs'), - spawn = require('./spawn'), - Version = require('./Version'); +var Q = require('q'); +var path = require('path'); +var shell = require('shelljs'); +var Version = require('./Version'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; function MSBuildTools (version, path) { this.version = version; @@ -30,9 +30,9 @@ function MSBuildTools (version, path) { } MSBuildTools.prototype.buildProject = function(projFile, buildType, buildarch, otherConfigProperties) { - console.log('Building project: ' + projFile); - console.log('\tConfiguration : ' + buildType); - console.log('\tPlatform : ' + buildarch); + events.emit('log', 'Building project: ' + projFile); + events.emit('log', '\tConfiguration : ' + buildType); + events.emit('log', '\tPlatform : ' + buildarch); var args = ['/clp:NoSummary;NoItemAndPropertyList;Verbosity=minimal', '/nologo', '/p:Configuration=' + buildType, @@ -45,7 +45,7 @@ MSBuildTools.prototype.buildProject = function(projFile, buildType, buildarch, o }); } - return spawn(path.join(this.path, 'msbuild'), [projFile].concat(args)); + return spawn(path.join(this.path, 'msbuild'), [projFile].concat(args), { stdio: 'inherit' }); }; // returns full path to msbuild tools required to build the project and tools version @@ -62,6 +62,7 @@ module.exports.findAvailableVersion = function () { module.exports.findAllAvailableVersions = function () { var versions = ['14.0', '12.0', '4.0']; + events.emit('verbose', 'Searching for available MSBuild versions...'); return Q.all(versions.map(checkMSBuildVersion)).then(function(unprocessedResults) { return unprocessedResults.filter(function(item) { @@ -71,27 +72,25 @@ module.exports.findAllAvailableVersions = function () { }; function checkMSBuildVersion(version) { - var deferred = Q.defer(); - exec('reg query HKLM\\SOFTWARE\\Microsoft\\MSBuild\\ToolsVersions\\' + version + ' /v MSBuildToolsPath') + return spawn('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\MSBuild\\ToolsVersions\\' + version, '/v', 'MSBuildToolsPath']) .then(function(output) { // fetch msbuild path from 'reg' output - var path = /MSBuildToolsPath\s+REG_SZ\s+(.*)/i.exec(output); - if (path) { - path = path[1]; + var toolsPath = /MSBuildToolsPath\s+REG_SZ\s+(.*)/i.exec(output); + if (toolsPath) { + toolsPath = toolsPath[1]; // CB-9565: Windows 10 invokes .NET Native compiler, which only runs on x86 arch, // so if we're running an x64 Node, make sure to use x86 tools. - if (version === '14.0' && path.indexOf('amd64') > -1) { - path = require('path').join(path, '..'); + if (version === '14.0' && toolsPath.indexOf('amd64') > -1) { + toolsPath = path.resolve(toolsPath, '..'); } - deferred.resolve(new MSBuildTools(version, path)); - return; + events.emit('verbose', 'Found MSBuild v' + version + ' at ' + toolsPath); + return new MSBuildTools(version, toolsPath); } - deferred.resolve(null); // not found - }, function (err) { + }) + .catch(function (err) { // if 'reg' exits with error, assume that registry key not found - deferred.resolve(null); + return; }); - return deferred.promise; } /// returns an array of available UAP Versions @@ -119,4 +118,5 @@ function getAvailableUAPVersions() { return result; } + module.exports.getAvailableUAPVersions = getAvailableUAPVersions; http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/PluginHandler.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/PluginHandler.js b/template/cordova/lib/PluginHandler.js new file mode 100644 index 0000000..43c4d03 --- /dev/null +++ b/template/cordova/lib/PluginHandler.js @@ -0,0 +1,233 @@ +/* + * + * 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; + +var handlers = { + 'source-file': { + install:function(obj, plugin, project, options) { + var dest = path.join('plugins', plugin.id, obj.targetDir || '', path.basename(obj.src)); + 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) { + // 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, getTargetConditions(obj)); + }, + uninstall:function(obj, plugin, project, options) { + 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; + + if(type === 'projectReference') { + project.addProjectReference(path.join(plugin.dir,src), getTargetConditions(obj)); + } + else { + var targetDir = obj.targetDir || ''; + // 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); + project.addReference(dest, getTargetConditions(obj)); + } + + }, + 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(plugin.dir, 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('<asset> tag without required "src" attribute. plugin=' + plugin.dir); + } + if (!obj.target) { + throw new CordovaError('<asset> tag without required "target" attribute'); + } + + var www = options.usePlatformWww ? project.platformWww : project.www; + copyFile(plugin.dir, obj.src, www, obj.target); + }, + uninstall:function(obj, plugin, project, options) { + var target = obj.target || obj.src; + + if (!target) throw new CordovaError('<asset> tag without required "target" attribute'); + + var www = options.usePlatformWww ? project.platformWww : project.www; + removeFile(www, target); + shell.rm('-Rf', path.resolve(www, '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.parse(obj.src).name); + + // 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 www = options.usePlatformWww ? project.platformWww : project.www; + var moduleDestination = path.resolve(www, 'plugins', plugin.id, obj.src); + shell.mkdir('-p', path.dirname(moduleDestination)); + fs.writeFileSync(moduleDestination, scriptContent, 'utf-8'); + }, + uninstall: function (obj, plugin, project, options) { + var pluginRelativePath = path.join('plugins', plugin.id, obj.src); + var www = options.usePlatformWww ? project.platformWww : project.www; + removeFileAndParents(www, 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('"' + src + '" not located within plugin!'); + + dest = path.resolve(project_dir, dest); + + // check that dest path is located in project directory + if (dest.indexOf(project_dir) !== 0) + throw new CordovaError('"' + dest + '" not located within 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; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
