From a677b8522d3d93a3c5af138f083caf2fe025e1b7 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Mon, 24 Aug 2026 15:02:13 -0600 Subject: [PATCH 1/2] Apply --sdkdependencies override to generated hybrid iOS Podfile The iOS SDK dependency for hybrid apps comes from a Podfile generated by the Cordova plugin, hardcoded to the dev branch. overrideSdkDependencies() only ever patched package.json, which that Podfile has no influence from, so --sdkdependencies was a silent no-op for hybrid+iOS. Rewrite the :git/:branch of any pod sourced from a repo named in the override, right after `cordova plugin add` creates the Podfile and before `cordova prepare` reparses/reinstalls it. --- shared/createHelper.js | 58 ++++++++++++++++++- unittests/createHelper.test.js | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/shared/createHelper.js b/shared/createHelper.js index 54572d2..494ae7c 100644 --- a/shared/createHelper.js +++ b/shared/createHelper.js @@ -72,6 +72,15 @@ function createHybridApp(config) { } utils.runProcessThrowError('cordova plugin add ' + config.cordovaPluginRepoUri + ' --force', config.projectDir); + // Override sdk dependencies in the generated iOS Podfile if an iOS override was provided. + // `cordova plugin add` above already ran `pod install` once against the plugin's hardcoded + // pod sources; the `cordova prepare` call further down always reparses and rewrites the + // Podfile (the plugin sets a deployment-target preference) and reruns `pod install`, which + // is what actually picks up this edit. + if (config.sdkdependencies && config.platform.split(',').includes('ios')) { + overrideHybridIosPodfile(path.join(config.projectDir, 'platforms', 'ios', 'Podfile'), config.sdkdependencies); + } + // Web directory - the home for the template var webDir = path.join(config.projectDir, 'www'); @@ -479,6 +488,52 @@ function overrideSdkDependencies(packageJsonPath, sdkDependenciesString) { } } +// +// Override sdk dependencies in the generated hybrid app's iOS Podfile. +// The Cordova plugin's plugin.xml hardcodes each Mobile SDK pod's :git repo and :branch => 'dev'; +// package.json has no influence over that Podfile, so overrideSdkDependencies() above is a no-op +// for hybrid+iOS. This rewrites the :git/:branch of any pod line sourced from a repo named in +// sdkDependenciesString (e.g. "SalesforceMobileSDK-iOS") to point at the given fork + branch. +// +function overrideHybridIosPodfile(podfilePath, sdkDependenciesString) { + try { + if (!fs.existsSync(podfilePath)) { + console.error(`Podfile not found at ${podfilePath}, skipping sdk dependencies override`); + return; + } + + let sdkDependencies = JSON.parse(sdkDependenciesString); + let originalContent = fs.readFileSync(podfilePath, 'utf8'); + + let updatedContent = originalContent.split('\n').map(function(line) { + if (!/^\s*pod\s+'/.test(line)) { + return line; + } + + for (var repoName in sdkDependencies) { + var gitRE = new RegExp(":git\\s*=>\\s*'[^']*\\/" + repoName + "(?:\\.git)?'"); + if (!gitRE.test(line) || !/:branch\s*=>\s*'[^']*'/.test(line)) { + continue; + } + + var parts = sdkDependencies[repoName].split('#'); + var newGitUrl = parts[0]; + var newBranch = parts.length > 1 ? parts[1] : 'dev'; + + line = line.replace(gitRE, ":git => '" + newGitUrl + "'"); + line = line.replace(/:branch\s*=>\s*'[^']*'/, ":branch => '" + newBranch + "'"); + } + + return line; + }).join('\n'); + + fs.writeFileSync(podfilePath, updatedContent, 'utf8'); + + } catch (err) { + console.error(`Failed to override sdk dependencies in Podfile: ${err}`); + } +} + // // Actually create app @@ -654,5 +709,6 @@ function validateCustomProperties(templateJsonPath, customProperties) { module.exports = { createApp, validateCustomProperties, - parseCallbackUrl + parseCallbackUrl, + overrideHybridIosPodfile }; diff --git a/unittests/createHelper.test.js b/unittests/createHelper.test.js index 2120e80..ff391c9 100644 --- a/unittests/createHelper.test.js +++ b/unittests/createHelper.test.js @@ -220,4 +220,106 @@ describe('createHelper', () => { expect(result.host).not.toBe('*'); }); }); + + describe('overrideHybridIosPodfile', () => { + const podfilePath = '/path/to/Podfile'; + + // Actual Podfile generated by the Cordova plugin (via `cordova plugin add`) for a + // hybrid_local iOS app against dev, captured from a real `test_force.js` run. + const originalPodfile = + "# DO NOT MODIFY -- auto-generated by Apache Cordova\n" + + "source 'https://cdn.cocoapods.org'\n" + + "source 'https://www.github.com/forcedotcom/SalesforceMobileSDK-iOS-Specs'\n" + + "platform :ios, '18.0'\n" + + "use_frameworks!\n" + + "target 'App' do\n" + + "\tproject 'App.xcodeproj'\n" + + "\tpod 'SalesforceHybridSDK', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS-Hybrid'\n" + + "\tpod 'MobileSync', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "\tpod 'SmartStore', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "\tpod 'SalesforceSDKCore', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "\tpod 'SalesforceAnalytics', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "\tpod 'SalesforceSDKCommon', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "\tpod 'Cordova', :tag => 'rel/8.1.0', :git => 'https://github.com/apache/cordova-ios'\n" + + "end\n"; + + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should rewrite the :git and :branch of every pod sourced from the overridden repo', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(originalPodfile); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const sdkDependencies = JSON.stringify({ + 'SalesforceMobileSDK-iOS': 'https://github.com/someFork/SalesforceMobileSDK-iOS#someBranch' + }); + + createHelper.overrideHybridIosPodfile(podfilePath, sdkDependencies); + + expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); + const [writtenPath, writtenContent] = writeFileSyncSpy.mock.calls[0]; + expect(writtenPath).toBe(podfilePath); + + // The five pods sourced from SalesforceMobileSDK-iOS are all repointed + const overriddenLines = writtenContent.split('\n').filter(line => + /^\s*pod\s+'(MobileSync|SmartStore|SalesforceSDKCore|SalesforceAnalytics|SalesforceSDKCommon)'/.test(line) + ); + expect(overriddenLines).toHaveLength(5); + overriddenLines.forEach(line => { + expect(line).toContain(":git => 'https://github.com/someFork/SalesforceMobileSDK-iOS'"); + expect(line).toContain(":branch => 'someBranch'"); + }); + + // SalesforceHybridSDK (different upstream repo) and Cordova are left untouched + expect(writtenContent).toContain("pod 'SalesforceHybridSDK', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS-Hybrid'"); + expect(writtenContent).toContain("pod 'Cordova', :tag => 'rel/8.1.0', :git => 'https://github.com/apache/cordova-ios'"); + }); + + it('should default to the dev branch when the override omits a branch', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(originalPodfile); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const sdkDependencies = JSON.stringify({ + 'SalesforceMobileSDK-iOS': 'https://github.com/someFork/SalesforceMobileSDK-iOS' + }); + + createHelper.overrideHybridIosPodfile(podfilePath, sdkDependencies); + + const writtenContent = writeFileSyncSpy.mock.calls[0][1]; + expect(writtenContent).toContain("pod 'SalesforceSDKCore', :branch => 'dev', :git => 'https://github.com/someFork/SalesforceMobileSDK-iOS'"); + }); + + it('should not touch pods sourced from a repo not named in the override', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(originalPodfile); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const sdkDependencies = JSON.stringify({ + 'SalesforceMobileSDK-iOS-Hybrid': 'https://github.com/someFork/SalesforceMobileSDK-iOS-Hybrid#someBranch' + }); + + createHelper.overrideHybridIosPodfile(podfilePath, sdkDependencies); + + const writtenContent = writeFileSyncSpy.mock.calls[0][1]; + expect(writtenContent).toContain("pod 'SalesforceHybridSDK', :branch => 'someBranch', :git => 'https://github.com/someFork/SalesforceMobileSDK-iOS-Hybrid'"); + expect(writtenContent).toContain("pod 'MobileSync', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'"); + }); + + it('should skip writing and log an error when the Podfile does not exist', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(false); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + createHelper.overrideHybridIosPodfile(podfilePath, JSON.stringify({ 'SalesforceMobileSDK-iOS': 'https://github.com/someFork/SalesforceMobileSDK-iOS#someBranch' })); + + expect(writeFileSyncSpy).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); + }); }); From cac3bd1234dac40016efa9e222120742763370b4 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Mon, 24 Aug 2026 18:17:12 -0600 Subject: [PATCH 2/2] Apply --sdkdependencies override to generated hybrid iOS Podfile (Warn instead of silently skipping when a matching pod isn't pinned with :branch) --- shared/createHelper.js | 6 +++++- unittests/createHelper.test.js | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/shared/createHelper.js b/shared/createHelper.js index 494ae7c..9fcc7da 100644 --- a/shared/createHelper.js +++ b/shared/createHelper.js @@ -512,7 +512,11 @@ function overrideHybridIosPodfile(podfilePath, sdkDependenciesString) { for (var repoName in sdkDependencies) { var gitRE = new RegExp(":git\\s*=>\\s*'[^']*\\/" + repoName + "(?:\\.git)?'"); - if (!gitRE.test(line) || !/:branch\s*=>\s*'[^']*'/.test(line)) { + if (!gitRE.test(line)) { + continue; + } + if (!/:branch\s*=>\s*'[^']*'/.test(line)) { + console.warn(`Skipping sdk dependencies override for ${repoName}: pod line is not pinned with :branch (e.g. it uses :tag or :commit instead), so it can't be safely rewritten: ${line.trim()}`); continue; } diff --git a/unittests/createHelper.test.js b/unittests/createHelper.test.js index ff391c9..22e9b14 100644 --- a/unittests/createHelper.test.js +++ b/unittests/createHelper.test.js @@ -245,6 +245,7 @@ describe('createHelper', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); }); afterEach(() => { @@ -312,6 +313,26 @@ describe('createHelper', () => { expect(writtenContent).toContain("pod 'MobileSync', :branch => 'dev', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'"); }); + it('should warn and leave a matching pod untouched when it is not pinned with :branch', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + const podfileWithTaggedPod = + "target 'App' do\n" + + "\tpod 'MobileSync', :tag => 'v13.2.0', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'\n" + + "end\n"; + jest.spyOn(fs, 'readFileSync').mockReturnValue(podfileWithTaggedPod); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + + const sdkDependencies = JSON.stringify({ + 'SalesforceMobileSDK-iOS': 'https://github.com/someFork/SalesforceMobileSDK-iOS#someBranch' + }); + + createHelper.overrideHybridIosPodfile(podfilePath, sdkDependencies); + + const writtenContent = writeFileSyncSpy.mock.calls[0][1]; + expect(writtenContent).toContain("pod 'MobileSync', :tag => 'v13.2.0', :git => 'https://github.com/forcedotcom/SalesforceMobileSDK-iOS'"); + expect(console.warn).toHaveBeenCalled(); + }); + it('should skip writing and log an error when the Podfile does not exist', () => { jest.spyOn(fs, 'existsSync').mockReturnValue(false); const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});