From 5589c15909156f2944abc020d647bef231aff114 Mon Sep 17 00:00:00 2001 From: Darryl Pogue Date: Fri, 22 Nov 2024 22:01:14 -0800 Subject: [PATCH] feat(spm): Update Package.swift on plugin add/remove Co-Authored-By: jcesarmobile --- lib/Api.js | 24 +++++- lib/SwiftPackage.js | 73 +++++++++++++++++ lib/plugman/pluginHandlers.js | 21 +++++ tests/spec/unit/Api.spec.js | 52 ++++++++++++ tests/spec/unit/Plugman/pluginHandler.spec.js | 57 +++++++------ tests/spec/unit/SwiftPackage.spec.js | 82 +++++++++++++++++++ .../App.xcodeproj/project.pbxproj | 20 ++++- .../CordovaPlugins/Package.swift | 37 +++++++++ .../CordovaPlugins/CordovaPlugins.swift | 21 +++++ .../Package.swift | 41 ++++++++++ .../plugin.xml | 33 ++++++++ .../src/ios/PackagePlugin.swift | 27 ++++++ tests/spec/unit/fixtures/test-Package.swift | 37 +++++++++ 13 files changed, 494 insertions(+), 31 deletions(-) create mode 100644 lib/SwiftPackage.js create mode 100644 tests/spec/unit/SwiftPackage.spec.js create mode 100644 tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Package.swift create mode 100644 tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Sources/CordovaPlugins/CordovaPlugins.swift create mode 100644 tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/Package.swift create mode 100644 tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/plugin.xml create mode 100644 tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/src/ios/PackagePlugin.swift create mode 100644 tests/spec/unit/fixtures/test-Package.swift diff --git a/lib/Api.js b/lib/Api.js index 407cd7fe2..4db043b0a 100644 --- a/lib/Api.js +++ b/lib/Api.js @@ -224,6 +224,7 @@ class Api { */ addPlugin (plugin, installOptions) { const xcodeproj = projectFile.parse(this.locations); + const { SwiftPackage, isSwiftPackagePlugin } = require('./SwiftPackage'); installOptions = installOptions || {}; installOptions.variables = installOptions.variables || {}; @@ -235,7 +236,14 @@ class Api { return PluginManager.get(this.platform, this.locations, xcodeproj) .addPlugin(plugin, installOptions) .then(() => { - if (plugin != null) { + if (plugin != null && isSwiftPackagePlugin(plugin)) { + const packagePath = path.join(this.locations.root, 'CordovaPlugins', 'Package.swift'); + const spm = new SwiftPackage(packagePath); + spm.addPlugin(plugin); + } + }) + .then(() => { + if (plugin != null && !isSwiftPackagePlugin(plugin)) { const headerTags = plugin.getHeaderFiles(this.platform); const bridgingHeaders = headerTags.filter(obj => obj.type === 'BridgingHeader'); if (bridgingHeaders.length > 0) { @@ -253,7 +261,7 @@ class Api { } }) .then(() => { - if (plugin != null) { + if (plugin != null && !isSwiftPackagePlugin(plugin)) { const podSpecs = plugin.getPodSpecs ? plugin.getPodSpecs(this.platform) : []; return this.addPodSpecs(plugin, podSpecs, installOptions); } @@ -277,11 +285,19 @@ class Api { */ removePlugin (plugin, uninstallOptions) { const xcodeproj = projectFile.parse(this.locations); + const { SwiftPackage, isSwiftPackagePlugin } = require('./SwiftPackage'); return PluginManager.get(this.platform, this.locations, xcodeproj) .removePlugin(plugin, uninstallOptions) .then(() => { - if (plugin != null) { + if (plugin != null && isSwiftPackagePlugin(plugin)) { + const packagePath = path.join(this.locations.root, 'CordovaPlugins', 'Package.swift'); + const spm = new SwiftPackage(packagePath); + spm.removePlugin(plugin); + } + }) + .then(() => { + if (plugin != null && !isSwiftPackagePlugin(plugin)) { const headerTags = plugin.getHeaderFiles(this.platform); const bridgingHeaders = headerTags.filter(obj => obj.type === 'BridgingHeader'); if (bridgingHeaders.length > 0) { @@ -299,7 +315,7 @@ class Api { } }) .then(() => { - if (plugin != null) { + if (plugin != null && !isSwiftPackagePlugin(plugin)) { const podSpecs = plugin.getPodSpecs ? plugin.getPodSpecs(this.platform) : []; return this.removePodSpecs(plugin, podSpecs, uninstallOptions); } diff --git a/lib/SwiftPackage.js b/lib/SwiftPackage.js new file mode 100644 index 000000000..460c86dc4 --- /dev/null +++ b/lib/SwiftPackage.js @@ -0,0 +1,73 @@ +/** + 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. +*/ + +const fs = require('node:fs'); +const CordovaError = require('cordova-common').CordovaError; + +class SwiftPackage { + constructor (packagePath) { + this.path = packagePath; + + if (!fs.existsSync(this.path)) { + throw new CordovaError('Package.swift is not found.'); + } + } + + _pluginReference (plugin) { + return ` +package.dependencies.append(.package(name: "${plugin.id}", path: "../../../plugins/${plugin.id}")) +package.targets.first?.dependencies.append(.product(name: "${plugin.id}", package: "${plugin.id}")) +`; + } + + addPlugin (plugin) { + const fd = fs.openSync(this.path, 'a'); + fs.writeFileSync(fd, this._pluginReference(plugin), 'utf8'); + fs.closeSync(fd); + } + + removePlugin (plugin) { + const fd = fs.openSync(this.path, 'r+'); + + let packageContent = fs.readFileSync(fd, 'utf8'); + packageContent = packageContent.replace(this._pluginReference(plugin), ''); + + fs.ftruncateSync(fd); + fs.writeFileSync(fd, packageContent, 'utf8'); + fs.closeSync(fd); + } +} + +// Use this as a hidden property on the plugin so that we can cache the result +// of the Swift Package check for the lifetime of that PluginInfo object, +// without caching it based on plugin name. +const _isSwiftPackage = Symbol('isSwiftPackage'); + +function isSwiftPackagePlugin (plugin) { + if (!plugin[_isSwiftPackage]) { + plugin[_isSwiftPackage] = plugin.getPlatforms().some((platform) => { + return platform.name === 'ios' && !!platform.package; + }); + } + + return plugin[_isSwiftPackage]; +} + +module.exports.SwiftPackage = SwiftPackage; +module.exports.isSwiftPackagePlugin = isSwiftPackagePlugin; diff --git a/lib/plugman/pluginHandlers.js b/lib/plugman/pluginHandlers.js index 572fba4e5..3ce239a7e 100644 --- a/lib/plugman/pluginHandlers.js +++ b/lib/plugman/pluginHandlers.js @@ -20,6 +20,7 @@ const path = require('node:path'); const util = require('node:util'); const events = require('cordova-common').events; const CordovaError = require('cordova-common').CordovaError; +const { isSwiftPackagePlugin } = require('../SwiftPackage'); // These frameworks are required by cordova-ios by default. We should never add/remove them. const keep_these_frameworks = [ @@ -31,22 +32,32 @@ const keep_these_frameworks = [ const handlers = { 'source-file': { install: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + installHelper('source-file', obj, plugin.dir, project.projectDir, plugin.id, options, project); }, uninstall: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + uninstallHelper('source-file', obj, project.projectDir, plugin.id, options, project); } }, 'header-file': { install: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + installHelper('header-file', obj, plugin.dir, project.projectDir, plugin.id, options, project); }, uninstall: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + uninstallHelper('header-file', obj, project.projectDir, plugin.id, options, project); } }, 'resource-file': { install: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + const src = obj.src; let target = obj.target; const srcFile = path.resolve(plugin.dir, src); @@ -67,6 +78,8 @@ const handlers = { copyFile(plugin.dir, src, project.projectDir, destFile, link); }, uninstall: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + const src = obj.src; let target = obj.target; @@ -81,6 +94,8 @@ const handlers = { }, framework: { // CB-5238 custom frameworks only install: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + const src = obj.src; const custom = !!(obj.custom); // convert to boolean (if truthy/falsy) const embed = !!(obj.embed); // convert to boolean (if truthy/falsy) @@ -129,6 +144,8 @@ const handlers = { events.emit('verbose', util.format('Custom framework added to project. %s -> %s', src, JSON.stringify(opt))); }, uninstall: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + const src = obj.src; if (!obj.custom) { // CB-9825 cocoapod integration for plugins @@ -167,6 +184,8 @@ const handlers = { }, asset: { install: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + if (!obj.src) { throw new CordovaError(generateAttributeError('src', 'asset', plugin.id)); } @@ -178,6 +197,8 @@ const handlers = { if (options && options.usePlatformWww) copyFile(plugin.dir, obj.src, project.platformWww, obj.target); }, uninstall: function (obj, plugin, project, options) { + if (isSwiftPackagePlugin(plugin)) return; + const target = obj.target; if (!target) { diff --git a/tests/spec/unit/Api.spec.js b/tests/spec/unit/Api.spec.js index ac6385847..2c3b90fe8 100644 --- a/tests/spec/unit/Api.spec.js +++ b/tests/spec/unit/Api.spec.js @@ -36,6 +36,7 @@ if (process.platform === 'darwin') { const projectFile = require('../../../lib/projectFile'); const BridgingHeader_mod = require('../../../lib/BridgingHeader.js'); +const SwiftPackage_mod = require('../../../lib/SwiftPackage.js'); const Podfile_mod = require('../../../lib/Podfile'); const PodsJson_mod = require('../../../lib/PodsJson'); const FIXTURES = path.join(__dirname, 'fixtures'); @@ -108,6 +109,7 @@ describe('Platform Api', () => { describe('addPlugin', () => { const my_plugin = { + getPlatforms () { return [{ name: 'ios' }]; }, getHeaderFiles: function () { return []; }, getFrameworks: function () { return []; }, getPodSpecs: function () { return []; } @@ -117,6 +119,7 @@ describe('Platform Api', () => { addPlugin: function () { return Promise.resolve(); } }); spyOn(BridgingHeader_mod, 'BridgingHeader'); + spyOn(SwiftPackage_mod, 'SwiftPackage'); spyOn(Podfile_mod, 'Podfile'); spyOn(PodsJson_mod, 'PodsJson'); }); @@ -145,6 +148,30 @@ describe('Platform Api', () => { }); }); }); + + describe('adding plugin with Swift package', () => { + let swiftPackage_mock; + const swift_plugin = { + id: 'swift_plugin', + getPlatforms () { return [{ name: 'ios', package: 'swift' }]; }, + getHeaderFiles: function () { return []; }, + getFrameworks: function () { return []; }, + getPodSpecs: function () { return []; } + }; + + beforeEach(() => { + swiftPackage_mock = jasmine.createSpyObj('swiftpackage mock', ['addPlugin']); + SwiftPackage_mod.SwiftPackage.and.returnValue(swiftPackage_mock); + }); + + it('should add the plugin reference to Package.swift', () => { + return api.addPlugin(swift_plugin) + .then(() => { + expect(swiftPackage_mock.addPlugin).toHaveBeenCalledWith(swift_plugin); + }); + }); + }); + describe('adding pods since the plugin contained ', () => { let podsjson_mock; let podfile_mock; @@ -301,6 +328,7 @@ describe('Platform Api', () => { }); describe('removePlugin', () => { const my_plugin = { + getPlatforms () { return [{ name: 'ios' }]; }, getHeaderFiles: function () { return []; }, getFrameworks: function () {}, getPodSpecs: function () { return []; } @@ -309,9 +337,33 @@ describe('Platform Api', () => { spyOn(PluginManager, 'get').and.returnValue({ removePlugin: function () { return Promise.resolve(); } }); + spyOn(SwiftPackage_mod, 'SwiftPackage'); spyOn(Podfile_mod, 'Podfile'); spyOn(PodsJson_mod, 'PodsJson'); }); + + describe('removing plugin with Swift package', () => { + let swiftPackage_mock; + const swift_plugin = { + id: 'swift_plugin', + getPlatforms () { return [{ name: 'ios', package: 'swift' }]; }, + getHeaderFiles: function () { return []; }, + getFrameworks: function () { return []; }, + getPodSpecs: function () { return []; } + }; + + beforeEach(() => { + swiftPackage_mock = jasmine.createSpyObj('swiftpackage mock', ['removePlugin']); + SwiftPackage_mod.SwiftPackage.and.returnValue(swiftPackage_mock); + }); + + it('should remove the plugin reference from Package.swift', () => { + return api.removePlugin(swift_plugin) + .then(() => { + expect(swiftPackage_mock.removePlugin).toHaveBeenCalledWith(swift_plugin); + }); + }); + }); describe('removing pods since the plugin contained ', () => { let podsjson_mock; let podfile_mock; diff --git a/tests/spec/unit/Plugman/pluginHandler.spec.js b/tests/spec/unit/Plugman/pluginHandler.spec.js index 368f5f5a6..e8ee1d0c6 100644 --- a/tests/spec/unit/Plugman/pluginHandler.spec.js +++ b/tests/spec/unit/Plugman/pluginHandler.spec.js @@ -39,6 +39,7 @@ const faultyplugin = path.join(FIXTURES, 'org.test.plugins.faultyplugin'); const dummyplugin = path.join(FIXTURES, 'org.test.plugins.dummyplugin'); const weblessplugin = path.join(FIXTURES, 'org.test.plugins.weblessplugin'); const embedlinkplugin = path.join(FIXTURES, 'org.test.plugins.embedlinkplugin'); +const swiftpackageplugin = path.join(FIXTURES, 'org.test.plugins.swiftpackageplugin'); const dummyPluginInfo = new PluginInfo(dummyplugin); const dummy_id = dummyPluginInfo.id; @@ -60,16 +61,12 @@ const weblessPluginInfo = new PluginInfo(weblessplugin); const embedlinkPluginInfo = new PluginInfo(embedlinkplugin); const embed_link_interaction_frameworks = embedlinkPluginInfo.getFrameworks('ios'); +const swiftpackagePluginInfo = new PluginInfo(swiftpackageplugin); + function copyArray (arr) { return Array.prototype.slice.call(arr, 0); } -function slashJoin () { - // In some places we need to use forward slash instead of path.join(). - // See CB-7311. - return Array.prototype.join.call(arguments, '/'); -} - describe('ios plugin handler', () => { let dummyProject; @@ -114,13 +111,13 @@ describe('ios plugin handler', () => { const source = copyArray(valid_source).filter(s => s.targetDir === undefined); install(source[0], dummyPluginInfo, dummyProject); expect(dummyProject.xcode.addSourceFile) - .toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'DummyPluginCommand.m'), {}); + .toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'DummyPluginCommand.m'), {}); }); it('Test 004 : should call into xcodeproj\'s addSourceFile appropriately when element has a target-dir', () => { const source = copyArray(valid_source).filter(s => s.targetDir !== undefined); install(source[0], dummyPluginInfo, dummyProject); expect(dummyProject.xcode.addSourceFile) - .toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m'), {}); + .toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m'), {}); }); it('Test 005 : should cp the file to the right target location when element has no target-dir', () => { const source = copyArray(valid_source).filter(s => s.targetDir === undefined); @@ -141,6 +138,13 @@ describe('ios plugin handler', () => { expect(dummyProject.xcode.addFramework) .toHaveBeenCalledWith(path.join('App', 'Plugins', dummy_id, 'SourceWithFramework.m'), { weak: false }); }); + + it('should not add source files for SPM plugins', () => { + const source = copyArray(swiftpackagePluginInfo.getSourceFiles('ios')); + install(source[0], swiftpackagePluginInfo, dummyProject); + + expect(dummyProject.xcode.addSourceFile).not.toHaveBeenCalled(); + }); }); describe('of elements', () => { @@ -169,13 +173,13 @@ describe('ios plugin handler', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir === undefined); install(headers[0], dummyPluginInfo, dummyProject); expect(dummyProject.xcode.addHeaderFile) - .toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'DummyPluginCommand.h')); + .toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'DummyPluginCommand.h')); }); it('Test 011 : should call into xcodeproj\'s addHeaderFile appropriately when element a target-dir', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir !== undefined); install(headers[0], dummyPluginInfo, dummyProject); expect(dummyProject.xcode.addHeaderFile) - .toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'targetDir', 'TargetDirTest.h')); + .toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.h')); }); it('Test 012 : should cp the file to the right target location when element has no target-dir', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir === undefined); @@ -407,17 +411,17 @@ describe('ios plugin handler', () => { }).xcode; // from org.test.plugins.dummyplugin - expect(xcode.hasFile(slashJoin('DummyPlugin.bundle'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.dummyplugin', 'DummyPluginCommand.h'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.dummyplugin', 'DummyPluginCommand.m'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.dummyplugin', 'targetDir', 'TargetDirTest.h'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.dummyplugin', 'targetDir', 'TargetDirTest.m'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('DummyPlugin.bundle'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.dummyplugin', 'DummyPluginCommand.h'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.dummyplugin', 'DummyPluginCommand.m'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.dummyplugin', 'targetDir', 'TargetDirTest.h'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.dummyplugin', 'targetDir', 'TargetDirTest.m'))).toEqual(jasmine.any(Object)); expect(xcode.hasFile('usr/lib/libsqlite3.dylib')).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('App', 'Plugins', 'org.test.plugins.dummyplugin', 'Custom.framework'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('App', 'Plugins', 'org.test.plugins.dummyplugin', 'Custom.framework'))).toEqual(jasmine.any(Object)); // from org.test.plugins.weblessplugin - expect(xcode.hasFile(slashJoin('WeblessPluginViewController.xib'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.weblessplugin', 'WeblessPluginCommand.h'))).toEqual(jasmine.any(Object)); - expect(xcode.hasFile(slashJoin('org.test.plugins.weblessplugin', 'WeblessPluginCommand.m'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('WeblessPluginViewController.xib'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.weblessplugin', 'WeblessPluginCommand.h'))).toEqual(jasmine.any(Object)); + expect(xcode.hasFile(path.posix.join('org.test.plugins.weblessplugin', 'WeblessPluginCommand.m'))).toEqual(jasmine.any(Object)); expect(xcode.hasFile('usr/lib/libsqlite3.dylib')).toEqual(jasmine.any(Object)); }); }); @@ -434,12 +438,12 @@ describe('ios plugin handler', () => { it('Test 029 : should call into xcodeproj\'s removeSourceFile appropriately when element has no target-dir', () => { const source = copyArray(valid_source).filter(s => s.targetDir === undefined); uninstall(source[0], dummyPluginInfo, dummyProject); - expect(dummyProject.xcode.removeSourceFile).toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'DummyPluginCommand.m')); + expect(dummyProject.xcode.removeSourceFile).toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'DummyPluginCommand.m')); }); it('Test 030 : should call into xcodeproj\'s removeSourceFile appropriately when element a target-dir', () => { const source = copyArray(valid_source).filter(s => s.targetDir !== undefined); uninstall(source[0], dummyPluginInfo, dummyProject); - expect(dummyProject.xcode.removeSourceFile).toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m')); + expect(dummyProject.xcode.removeSourceFile).toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m')); }); it('Test 031 : should rm the file from the right target location when element has no target-dir', () => { const source = copyArray(valid_source).filter(s => s.targetDir === undefined); @@ -458,6 +462,13 @@ describe('ios plugin handler', () => { uninstall(source[0], dummyPluginInfo, dummyProject); expect(dummyProject.xcode.removeFramework).toHaveBeenCalledWith(path.join('App', 'Plugins', dummy_id, 'SourceWithFramework.m')); }); + + it('should not remove source files for SPM plugins', () => { + const source = copyArray(swiftpackagePluginInfo.getSourceFiles('ios')); + uninstall(source[0], swiftpackagePluginInfo, dummyProject); + + expect(dummyProject.xcode.removeSourceFile).not.toHaveBeenCalled(); + }); }); describe('of elements', () => { @@ -469,12 +480,12 @@ describe('ios plugin handler', () => { it('Test 034 : should call into xcodeproj\'s removeHeaderFile appropriately when element has no target-dir', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir === undefined); uninstall(headers[0], dummyPluginInfo, dummyProject); - expect(dummyProject.xcode.removeHeaderFile).toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'DummyPluginCommand.h')); + expect(dummyProject.xcode.removeHeaderFile).toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'DummyPluginCommand.h')); }); it('Test 035 : should call into xcodeproj\'s removeHeaderFile appropriately when element a target-dir', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir !== undefined); uninstall(headers[0], dummyPluginInfo, dummyProject); - expect(dummyProject.xcode.removeHeaderFile).toHaveBeenCalledWith(slashJoin('Plugins', dummy_id, 'targetDir', 'TargetDirTest.h')); + expect(dummyProject.xcode.removeHeaderFile).toHaveBeenCalledWith(path.posix.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.h')); }); it('Test 036 : should rm the file from the right target location', () => { const headers = copyArray(valid_headers).filter(s => s.targetDir !== undefined); diff --git a/tests/spec/unit/SwiftPackage.spec.js b/tests/spec/unit/SwiftPackage.spec.js new file mode 100644 index 000000000..da11dc461 --- /dev/null +++ b/tests/spec/unit/SwiftPackage.spec.js @@ -0,0 +1,82 @@ +/** + 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. +*/ + +const fs = require('node:fs'); +const path = require('node:path'); +const tmp = require('tmp'); +const SwiftPackage = require('../../../lib/SwiftPackage.js').SwiftPackage; + +tmp.setGracefulCleanup(); + +const fixturePackage = fs.readFileSync(path.join(__dirname, 'fixtures', 'test-Package.swift'), 'utf-8'); + +describe('SwiftPackage', () => { + it('should error if Package.swift file does not exist', () => { + expect(() => { + const _ = new SwiftPackage('dummypath'); + expect(_).not.toEqual(null); // To avoid ESLINT error "Do not use 'new' for side effects" + }).toThrow(); + }); + + describe('addPlugin', () => { + const my_plugin = { + id: 'my-plugin' + }; + + let pkg; + let tmpFile; + beforeEach(() => { + tmpFile = tmp.fileSync({ discardDescriptor: true }); + fs.writeFileSync(tmpFile.name, fixturePackage, 'utf8'); + + pkg = new SwiftPackage(tmpFile.name); + }); + + it('should add plugin references to the package file', () => { + pkg.addPlugin(my_plugin); + + const content = fs.readFileSync(tmpFile.name, 'utf8'); + expect(content).toContain('.package(name: "my-plugin"'); + expect(content).toContain('.product(name: "my-plugin", package: "my-plugin")'); + }); + }); + + describe('removePlugin', () => { + const my_plugin = { + id: 'my-plugin' + }; + + let pkg; + let tmpFile; + beforeEach(() => { + tmpFile = tmp.fileSync({ discardDescriptor: true }); + + pkg = new SwiftPackage(tmpFile.name); + fs.writeFileSync(tmpFile.name, fixturePackage + pkg._pluginReference(my_plugin), 'utf8'); + }); + + it('should add plugin references to the package file', () => { + pkg.removePlugin(my_plugin); + + const content = fs.readFileSync(tmpFile.name, 'utf8'); + expect(content).not.toContain('.package(name: "my-plugin"'); + expect(content).not.toContain('.product(name: "my-plugin", package: "my-plugin")'); + }); + }); +}); diff --git a/tests/spec/unit/fixtures/ios-config-xml/App.xcodeproj/project.pbxproj b/tests/spec/unit/fixtures/ios-config-xml/App.xcodeproj/project.pbxproj index 597e416f9..da8158b2d 100755 --- a/tests/spec/unit/fixtures/ios-config-xml/App.xcodeproj/project.pbxproj +++ b/tests/spec/unit/fixtures/ios-config-xml/App.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 90CBB5282C06968500B805A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CBB5272C06968500B805A2 /* AppDelegate.swift */; }; 90CBB52A2C06968500B805A2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CBB5292C06968500B805A2 /* SceneDelegate.swift */; }; 90CBB52C2C06968500B805A2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CBB52B2C06968500B805A2 /* ViewController.swift */; }; + 90D82ABD2CF19AEA001383CF /* CordovaPlugins in Frameworks */ = {isa = PBXBuildFile; productRef = 90D82ABC2CF19AEA001383CF /* CordovaPlugins */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -80,6 +81,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 90D82ABD2CF19AEA001383CF /* CordovaPlugins in Frameworks */, 90A914592CA3D370003DB979 /* CordovaLib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -191,6 +193,7 @@ name = App; packageProductDependencies = ( 90A914582CA3D370003DB979 /* CordovaLib */, + 90D82ABC2CF19AEA001383CF /* CordovaPlugins */, ); productName = App; productReference = 90BD9B6C2C06907D000DEBAB /* App.app */; @@ -222,6 +225,7 @@ mainGroup = 90BD9B632C06907D000DEBAB /* CustomTemplate */; packageReferences = ( 90A914572CA3D370003DB979 /* XCLocalSwiftPackageReference "../../../cordova-ios" */, + 90D82ABB2CF19AEA001383CF /* XCLocalSwiftPackageReference "CordovaPlugins" */, ); productRefGroup = 90BD9B6D2C06907D000DEBAB /* Products */; projectDirPath = ""; @@ -317,8 +321,8 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; "DEPLOYMENT_LOCATION[sdk=iphonesimulator*]" = YES; - "DEPLOYMENT_LOCATION[sdk=xrsimulator*]" = YES; "DEPLOYMENT_LOCATION[sdk=macosx*]" = YES; + "DEPLOYMENT_LOCATION[sdk=xrsimulator*]" = YES; DSTROOT = "$(SRCROOT)/build"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -338,8 +342,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; "INSTALL_PATH[sdk=iphonesimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; - "INSTALL_PATH[sdk=xrsimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; "INSTALL_PATH[sdk=macosx*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; + "INSTALL_PATH[sdk=xrsimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -391,8 +395,8 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; "DEPLOYMENT_LOCATION[sdk=iphonesimulator*]" = YES; - "DEPLOYMENT_LOCATION[sdk=xrsimulator*]" = YES; "DEPLOYMENT_LOCATION[sdk=macosx*]" = YES; + "DEPLOYMENT_LOCATION[sdk=xrsimulator*]" = YES; DSTROOT = "$(SRCROOT)/build"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -406,8 +410,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; "INSTALL_PATH[sdk=iphonesimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; - "INSTALL_PATH[sdk=xrsimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; "INSTALL_PATH[sdk=macosx*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; + "INSTALL_PATH[sdk=xrsimulator*]" = "$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; @@ -525,6 +529,10 @@ isa = XCLocalSwiftPackageReference; relativePath = "../../../../../../cordova-ios"; }; + 90D82ABB2CF19AEA001383CF /* XCLocalSwiftPackageReference "CordovaPlugins" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = CordovaPlugins; + }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -532,6 +540,10 @@ isa = XCSwiftPackageProductDependency; productName = CordovaLib; }; + 90D82ABC2CF19AEA001383CF /* CordovaPlugins */ = { + isa = XCSwiftPackageProductDependency; + productName = CordovaPlugins; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 90BD9B642C06907D000DEBAB /* Project object */; diff --git a/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Package.swift b/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Package.swift new file mode 100644 index 000000000..705e2778e --- /dev/null +++ b/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:5.5 + +/* + 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. +*/ + +import PackageDescription + +let package = Package( + name: "CordovaPlugins", + platforms: [ + .iOS(.v13), + .macCatalyst(.v13) + ], + products: [ + .library(name: "CordovaPlugins", targets: ["CordovaPlugins"]) + ], + targets: [ + .target(name: "CordovaPlugins") + ] +) + diff --git a/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Sources/CordovaPlugins/CordovaPlugins.swift b/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Sources/CordovaPlugins/CordovaPlugins.swift new file mode 100644 index 000000000..92baa9178 --- /dev/null +++ b/tests/spec/unit/fixtures/ios-config-xml/CordovaPlugins/Sources/CordovaPlugins/CordovaPlugins.swift @@ -0,0 +1,21 @@ +/* + 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. +*/ + +// We need something here so that the empty package will compile successfully +public let isCordovaApp = true diff --git a/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/Package.swift b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/Package.swift new file mode 100644 index 000000000..e11aa46d0 --- /dev/null +++ b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version:5.5 + +/* + 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. +*/ + +import PackageDescription + +let package = Package( + name: "PackagePlugin", + platforms: [ + .iOS(.v13), + .macCatalyst(.v13) + ], + products: [ + .library(name: "PackagePlugin", targets: ["PackagePlugin"]) + ], + targets: [ + .target( + name: "PackagePlugin", + path: "src", + sources: ["ios"] + ) + ] +) + diff --git a/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/plugin.xml b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/plugin.xml new file mode 100644 index 000000000..08ddec80f --- /dev/null +++ b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/plugin.xml @@ -0,0 +1,33 @@ + + + + Swift Package Plugin + + + + + + + + + + + + diff --git a/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/src/ios/PackagePlugin.swift b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/src/ios/PackagePlugin.swift new file mode 100644 index 000000000..311031daf --- /dev/null +++ b/tests/spec/unit/fixtures/org.test.plugins.swiftpackageplugin/src/ios/PackagePlugin.swift @@ -0,0 +1,27 @@ +/* + 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. +*/ + +import Cordova + +@objc +class PackagePlugin : CDVPlugin { + override func pluginInitialize() { + NSLog("Initialized Swift Package Plugin"); + } +} diff --git a/tests/spec/unit/fixtures/test-Package.swift b/tests/spec/unit/fixtures/test-Package.swift new file mode 100644 index 000000000..705e2778e --- /dev/null +++ b/tests/spec/unit/fixtures/test-Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:5.5 + +/* + 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. +*/ + +import PackageDescription + +let package = Package( + name: "CordovaPlugins", + platforms: [ + .iOS(.v13), + .macCatalyst(.v13) + ], + products: [ + .library(name: "CordovaPlugins", targets: ["CordovaPlugins"]) + ], + targets: [ + .target(name: "CordovaPlugins") + ] +) +