diff --git a/bin/templates/scripts/cordova/Api.js b/bin/templates/scripts/cordova/Api.js index bc5f10a3e..9a1da6aff 100644 --- a/bin/templates/scripts/cordova/Api.js +++ b/bin/templates/scripts/cordova/Api.js @@ -233,6 +233,26 @@ Api.prototype.addPlugin = function (plugin, installOptions) { return PluginManager.get(self.platform, self.locations, xcodeproj) .addPlugin(plugin, installOptions) + .then(function () { + if (plugin != null) { + var headerTags = plugin.getHeaderFiles(self.platform); + var bridgingHeaders = headerTags.filter(function (obj) { + return (obj.type === 'BridgingHeader'); + }); + if (bridgingHeaders.length > 0) { + var project_dir = self.locations.root; + var project_name = self.locations.xcodeCordovaProj.split('/').pop(); + var BridgingHeader = require('./lib/BridgingHeader').BridgingHeader; + var bridgingHeaderFile = new BridgingHeader(path.join(project_dir, project_name, 'Bridging-Header.h')); + events.emit('verbose', 'Adding Bridging-Headers since the plugin contained with type="BridgingHeader"'); + bridgingHeaders.forEach(function (obj) { + var bridgingHeaderPath = path.basename(obj.src); + bridgingHeaderFile.addHeader(plugin.id, bridgingHeaderPath); + }); + bridgingHeaderFile.write(); + } + } + }) .then(function () { var frameworkTags = plugin.getFrameworks(self.platform); var frameworkPods = frameworkTags.filter(function (obj) { @@ -318,6 +338,26 @@ Api.prototype.removePlugin = function (plugin, uninstallOptions) { return PluginManager.get(self.platform, self.locations, xcodeproj) .removePlugin(plugin, uninstallOptions) + .then(function () { + if (plugin != null) { + var headerTags = plugin.getHeaderFiles(self.platform); + var bridgingHeaders = headerTags.filter(function (obj) { + return (obj.type === 'BridgingHeader'); + }); + if (bridgingHeaders.length > 0) { + var project_dir = self.locations.root; + var project_name = self.locations.xcodeCordovaProj.split('/').pop(); + var BridgingHeader = require('./lib/BridgingHeader').BridgingHeader; + var bridgingHeaderFile = new BridgingHeader(path.join(project_dir, project_name, 'Bridging-Header.h')); + events.emit('verbose', 'Removing Bridging-Headers since the plugin contained with type="BridgingHeader"'); + bridgingHeaders.forEach(function (obj) { + var bridgingHeaderPath = path.basename(obj.src); + bridgingHeaderFile.removeHeader(plugin.id, bridgingHeaderPath); + }); + bridgingHeaderFile.write(); + } + } + }) .then(function () { var frameworkTags = plugin.getFrameworks(self.platform); var frameworkPods = frameworkTags.filter(function (obj) { diff --git a/bin/templates/scripts/cordova/lib/BridgingHeader.js b/bin/templates/scripts/cordova/lib/BridgingHeader.js new file mode 100644 index 000000000..e20383515 --- /dev/null +++ b/bin/templates/scripts/cordova/lib/BridgingHeader.js @@ -0,0 +1,125 @@ +/* + 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. +*/ +'use strict'; + +var fs = require('fs'); +var CordovaError = require('cordova-common').CordovaError; + +function BridgingHeader (bridgingHeaderPath) { + this.path = bridgingHeaderPath; + this.bridgingHeaders = null; + if (!fs.existsSync(this.path)) { + throw new CordovaError('BridgingHeader.h is not found.'); + } + this.bridgingHeaders = this.__parseForBridgingHeader(fs.readFileSync(this.path, 'utf8')); +} + +BridgingHeader.prototype.addHeader = function (plugin_id, header_path) { + this.bridgingHeaders.push({type: 'code', code: '#import "' + header_path + '"\n'}); +}; + +BridgingHeader.prototype.removeHeader = function (plugin_id, header_path) { + this.bridgingHeaders = this.bridgingHeaders.filter(function (line) { + if (this.found) { + return true; + } + if (line.type === 'code') { + var re = new RegExp('#import\\s+"' + preg_quote(header_path) + '"(\\s*|\\s.+)(\\n|$)'); + if (re.test(line.code)) { + this.found = true; + return false; + } + } + return true; + }, {found: false}); +}; + +BridgingHeader.prototype.write = function () { + var text = this.__stringifyForBridgingHeader(this.bridgingHeaders); + fs.writeFileSync(this.path, text, 'utf8'); +}; + +BridgingHeader.prototype.__stringifyForBridgingHeader = function (bridgingHeaders) { + return bridgingHeaders.map(function (obj) { + return obj.code; + }).join(''); +}; + +BridgingHeader.prototype.__parseForBridgingHeader = function (text) { + var i = 0; + var list = []; + var type = 'code'; + var start = 0; + while (i < text.length) { + switch (type) { + case 'comment': + if (i + 1 < text.length && text[i] === '*' && text[i + 1] === '/') { + i += 2; + list.push({type: type, code: text.slice(start, i)}); + type = 'code'; + start = i; + } else { + i += 1; + } + break; + case 'line-comment': + if (i < text.length && text[i] === '\n') { + i += 1; + list.push({type: type, code: text.slice(start, i)}); + type = 'code'; + start = i; + } else { + i += 1; + } + break; + case 'code': + default: + if (i + 1 < text.length && text[i] === '/' && text[i + 1] === '*') { // comment + if (start < i) { + list.push({type: type, code: text.slice(start, i)}); + } + type = 'comment'; + start = i; + } else if (i + 1 < text.length && text[i] === '/' && text[i + 1] === '/') { // line comment + if (start < i) { + list.push({type: type, code: text.slice(start, i)}); + } + type = 'line-comment'; + start = i; + } else if (i < text.length && text[i] === '\n') { + i += 1; + list.push({type: type, code: text.slice(start, i)}); + start = i; + } else { + i += 1; + } + break; + } + } + if (start < i) { + list.push({type: type, code: text.slice(start, i)}); + } + return list; +}; + +function preg_quote (str, delimiter) { + return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&'); +} + +module.exports.BridgingHeader = BridgingHeader; diff --git a/bin/templates/scripts/cordova/lib/prepare.js b/bin/templates/scripts/cordova/lib/prepare.js index de345dd55..10f14cc45 100644 --- a/bin/templates/scripts/cordova/lib/prepare.js +++ b/bin/templates/scripts/cordova/lib/prepare.js @@ -277,10 +277,11 @@ function handleBuildSettings (platformConfig, locations, infoPlist) { var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios')); var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios'); var needUpdatedBuildSettingsForLaunchStoryboard = checkIfBuildSettingsNeedUpdatedForLaunchStoryboard(platformConfig, infoPlist); + var swiftVersion = platformConfig.getPreference('SwiftVersion', 'ios'); // no build settings provided and we don't need to update build settings for launch storyboards, // then we don't need to parse and update .pbxproj file - if (!targetDevice && !deploymentTarget && !needUpdatedBuildSettingsForLaunchStoryboard) { + if (!targetDevice && !deploymentTarget && !needUpdatedBuildSettingsForLaunchStoryboard && !swiftVersion) { return Q(); } @@ -302,6 +303,11 @@ function handleBuildSettings (platformConfig, locations, infoPlist) { proj.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget); } + if (swiftVersion) { + events.emit('verbose', 'Set SwiftVersion to "' + swiftVersion + '".'); + proj.updateBuildProperty('SWIFT_VERSION', swiftVersion); + } + updateBuildSettingsForLaunchStoryboard(proj, platformConfig, infoPlist); fs.writeFileSync(locations.pbxproj, proj.writeSync(), 'utf-8'); diff --git a/tests/spec/unit/Api.spec.js b/tests/spec/unit/Api.spec.js index eef7f7275..2e8db2da5 100644 --- a/tests/spec/unit/Api.spec.js +++ b/tests/spec/unit/Api.spec.js @@ -35,6 +35,7 @@ if (process.platform === 'darwin') { } var projectFile = require('../../../bin/templates/scripts/cordova/lib/projectFile'); +var BridgingHeader_mod = require('../../../bin/templates/scripts/cordova/lib/BridgingHeader.js'); var Podfile_mod = require('../../../bin/templates/scripts/cordova/lib/Podfile'); var PodsJson_mod = require('../../../bin/templates/scripts/cordova/lib/PodsJson'); var Q = require('q'); @@ -158,12 +159,14 @@ describe('Platform Api', function () { describe('addPlugin', function () { var my_plugin = { + getHeaderFiles: function () { return []; }, getFrameworks: function () {} }; beforeEach(function () { spyOn(PluginManager, 'get').and.returnValue({ addPlugin: function () { return Q(); } }); + spyOn(BridgingHeader_mod, 'BridgingHeader'); spyOn(Podfile_mod, 'Podfile'); spyOn(PodsJson_mod, 'PodsJson'); }); @@ -172,6 +175,32 @@ describe('Platform Api', function () { api.addPlugin('my cool plugin', opts); expect(opts.variables.PACKAGE_NAME).toEqual('ios.cordova.io'); }); + describe('with header-file of `BridgingHeader` type', function () { + var bridgingHeader_mock; + var my_bridgingHeader_json = { + type: 'BridgingHeader', + src: 'bridgingHeaderSource!' + }; + beforeEach(function () { + bridgingHeader_mock = jasmine.createSpyObj('bridgingHeader mock', ['addHeader', 'write']); + spyOn(my_plugin, 'getFrameworks').and.returnValue([]); + spyOn(my_plugin, 'getHeaderFiles').and.returnValue([my_bridgingHeader_json]); + BridgingHeader_mod.BridgingHeader.and.callFake(function () { + return bridgingHeader_mock; + }); + }); + it('should add BridgingHeader', function (done) { + api.addPlugin(my_plugin) + .then(function () { + expect(bridgingHeader_mock.addHeader).toHaveBeenCalledWith(my_plugin.id, 'bridgingHeaderSource!'); + expect(bridgingHeader_mock.write).toHaveBeenCalled(); + }).fail(function (err) { + fail('unexpected addPlugin fail handler invoked'); + console.error(err); + }).done(done); + }); + + }); describe('with frameworks of `podspec` type', function () { var podsjson_mock; var podfile_mock; diff --git a/tests/spec/unit/BridgingHeader.spec.js b/tests/spec/unit/BridgingHeader.spec.js new file mode 100644 index 000000000..99760ac72 --- /dev/null +++ b/tests/spec/unit/BridgingHeader.spec.js @@ -0,0 +1,111 @@ +var fs = require('fs'); +var path = require('path'); + +var BridgingHeader = require(path.resolve(path.join(__dirname, '..', '..', '..', 'bin', 'templates', 'scripts', 'cordova', 'lib', 'BridgingHeader.js'))).BridgingHeader; +var fixtureBridgingHeader = fs.readFileSync(path.resolve(__dirname, 'fixtures', 'test-Bridging-Header.h'), 'utf-8'); + +describe('unit tests for BridgingHeader module', function () { + var existsSyncSpy; + var readFileSyncSpy; + var writeFileSyncSpy; + var dummy_path = 'dummy_path'; + var dummy_plugin = { id: 'dummy_plugin', header_path: 'dummy_header_path' }; + var dummy_plugin2 = { id: 'dummy_plugin2', header_path: 'dummy_header_path2' }; + var headerImportText = function (header_path) { return '#import "' + header_path + '"'; }; + + beforeEach(function () { + existsSyncSpy = spyOn(fs, 'existsSync'); + readFileSyncSpy = spyOn(fs, 'readFileSync'); + writeFileSyncSpy = spyOn(fs, 'writeFileSync'); + }); + it('Test#001 : should error if BridgingHeader file does not exist', function () { + existsSyncSpy.and.returnValue(false); + expect(function () { + var _ = new BridgingHeader(fixtureBridgingHeader); + expect(_).not.toEqual(null); // To avoid ESLINT error "Do not use 'new' for side effects" + }).toThrow(); + }); + it('Test#002 : load BridgingHeader file', function () { + existsSyncSpy.and.returnValue(true); + readFileSyncSpy.and.returnValue(fixtureBridgingHeader); + + var bridgingHeader = new BridgingHeader(dummy_path); + expect(bridgingHeader.path).toEqual(dummy_path); + expect(bridgingHeader).not.toEqual(null); + }); + it('Test#003 : add and remove a BridgingHeader', function () { + var result_json = null; + var text_list = null; + var bridgingHeaderFileContent = fixtureBridgingHeader; + existsSyncSpy.and.returnValue(true); + readFileSyncSpy.and.callFake(function (read_path, charset) { + return bridgingHeaderFileContent; + }); + writeFileSyncSpy.and.callFake(function (write_path, text, charset) { + result_json = {write_path: write_path, text: text, charset: charset}; + }); + + var bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.addHeader(dummy_plugin.id, dummy_plugin.header_path); + bridgingHeader.write(); + expect(result_json).not.toEqual(null); + expect(result_json.write_path).toEqual(dummy_path); + expect(result_json.text).not.toEqual(null); + expect(result_json.charset).toEqual('utf8'); + text_list = result_json.text.split('\n'); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin.header_path); }).length).toEqual(1); + + bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.removeHeader(dummy_plugin.id, dummy_plugin.header_path); + bridgingHeader.write(); + expect(result_json).not.toEqual(null); + expect(result_json.write_path).toEqual(dummy_path); + expect(result_json.text).not.toEqual(null); + expect(result_json.charset).toEqual('utf8'); + text_list = result_json.text.split('\n'); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin.header_path); }).length).toEqual(0); + }); + it('Test#004 : add and remove two BridgingHeaders', function () { + var result_json = null; + var text_list = null; + var bridgingHeaderFileContent = fixtureBridgingHeader; + existsSyncSpy.and.returnValue(true); + readFileSyncSpy.and.callFake(function (read_path, charset) { + return bridgingHeaderFileContent; + }); + writeFileSyncSpy.and.callFake(function (write_path, text, charset) { + bridgingHeaderFileContent = text; + result_json = {write_path: write_path, text: text, charset: charset}; + }); + + var bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.addHeader(dummy_plugin.id, dummy_plugin.header_path); + bridgingHeader.write(); + + bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.addHeader(dummy_plugin2.id, dummy_plugin2.header_path); + bridgingHeader.write(); + + text_list = result_json.text.split('\n'); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin.header_path); }).length).toEqual(1); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin2.header_path); }).length).toEqual(1); + + bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.removeHeader(dummy_plugin.id, dummy_plugin.header_path); + bridgingHeader.write(); + + text_list = result_json.text.split('\n'); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin.header_path); }).length).toEqual(0); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin2.header_path); }).length).toEqual(1); + + bridgingHeader = new BridgingHeader(dummy_path); + bridgingHeader.removeHeader(dummy_plugin2.id, dummy_plugin2.header_path); + bridgingHeader.write(); + + text_list = result_json.text.split('\n'); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin.header_path); }).length).toEqual(0); + expect(text_list.filter(function (line) { return line === headerImportText(dummy_plugin2.header_path); }).length).toEqual(0); + + }); + +}); diff --git a/tests/spec/unit/fixtures/test-Bridging-Header.h b/tests/spec/unit/fixtures/test-Bridging-Header.h new file mode 100644 index 000000000..9c9c87cd5 --- /dev/null +++ b/tests/spec/unit/fixtures/test-Bridging-Header.h @@ -0,0 +1,30 @@ +/* + 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. + */ +// +// Bridging-Header.h +// __PROJECT_NAME__ +// +// Created by ___FULLUSERNAME___ on ___DATE___. +// Copyright ___ORGANIZATIONNAME___ ___YEAR___. All rights reserved. +// +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import +#import "CDVSwift22Object.h" +#import "CDVSwift2Object.h" diff --git a/tests/spec/unit/fixtures/test-config-3.xml b/tests/spec/unit/fixtures/test-config-3.xml new file mode 100644 index 000000000..9bea9d6f3 --- /dev/null +++ b/tests/spec/unit/fixtures/test-config-3.xml @@ -0,0 +1,25 @@ + + + Hello Cordova + + A sample Apache Cordova application that responds to the deviceready event. + + + Apache Cordova Team + + + + + + + + + + + + + + + +   + diff --git a/tests/spec/unit/prepare.spec.js b/tests/spec/unit/prepare.spec.js index 8c12305f1..4705d715f 100644 --- a/tests/spec/unit/prepare.spec.js +++ b/tests/spec/unit/prepare.spec.js @@ -44,6 +44,7 @@ var ConfigParser = require('cordova-common').ConfigParser; // Create a real config object before mocking out everything. var cfg = new ConfigParser(path.join(FIXTURES, 'test-config.xml')); var cfg2 = new ConfigParser(path.join(FIXTURES, 'test-config-2.xml')); +var cfg3 = new ConfigParser(path.join(FIXTURES, 'test-config-3.xml')); function wrapper (p, done, post) { p.then(post, function (err) { @@ -626,6 +627,41 @@ describe('prepare', function () { cfg2.name = cfg2OriginalName; }); }); + it('should write SwiftVersion preference (4.1)', function (done) { + var cfg3OriginalName = cfg3.name; + cfg3.name = function () { return 'SampleApp'; }; // new config does *not* have a name change + writeFileSyncSpy.and.callThrough(); + wrapper(updateProject(cfg3, p.locations), done, function () { + var xcode = require('xcode'); + var proj = new xcode.project(p.locations.pbxproj); /* eslint new-cap : 0 */ + proj.parseSync(); + var prop = proj.getBuildProperty('SWIFT_VERSION'); + expect(prop).toEqual('4.1'); + + // restore cfg2 original name + cfg3.name = cfg3OriginalName; + }); + }); + it('should write SwiftVersion preference (3.3)', function (done) { + var cfg3OriginalName = cfg3.name; + cfg3.name = function () { return 'SampleApp'; }; // new config does *not* have a name change + var pref = cfg3.doc.findall('platform[@name=\'ios\']/preference').filter(function (elem) { + return elem.attrib.name.toLowerCase() === 'swiftversion'; + })[0]; + var prefOriginalSwiftVersion = pref.attrib.value; + pref.attrib.value = '3.3'; + writeFileSyncSpy.and.callThrough(); + wrapper(updateProject(cfg3, p.locations), done, function () { + var xcode = require('xcode'); + var proj = new xcode.project(p.locations.pbxproj); /* eslint new-cap : 0 */ + proj.parseSync(); + var prop = proj.getBuildProperty('SWIFT_VERSION'); + expect(prop).toEqual('3.3'); + // restore cfg2 original name + cfg3.name = cfg3OriginalName; + pref.attrib.value = prefOriginalSwiftVersion; + }); + }); it('Test#002 : should write out the app id to info plist as CFBundleIdentifier', function (done) { var orig = cfg.getAttribute;