From 4c196aec902fa0b9187e11f964aed0941778a2ae Mon Sep 17 00:00:00 2001 From: Miron Pawlik Date: Tue, 17 Oct 2017 21:19:53 -0700 Subject: [PATCH] Make `react-native link` play nicely with CocoaPods-based iOS projects. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The core React Native codebase already has full support for CocoaPods. However, `react-native link` doesn’t play nicely with CocoaPods, so installing third-party libs from the RN ecosystem is really hard. This change will allow to link projects that contains its own `.podspec` file to CocoaPods-based projects. In case `link` detect `Podfile` in `iOS` directory, it will look for related `.podspec` file in linked project directory, and add it to `Podfile`. If `Podfile` and `.podspec` files are not present, it will fall back to previous implementation. **Test Plan** 1. Build a React Native project where the iOS part uses CocoaPods to manage its dependencies. The most common scenario here is to have React Native be a Pod dependency, among others. 2. Install a RN-related library, that contains `.podspec` file, with `react-native link` (as an example it could be: [react-native-maps](https://github.com/airbnb/react-native-maps) 3. Building the resulting iOS workspace should succeed (and there should be new entry in `Podfile`) Closes https://github.com/facebook/react-native/pull/15460 Differential Revision: D6078649 Pulled By: hramos fbshipit-source-id: 9651085875892fd66299563ca0e42fb2bcc00825 --- docs/LinkingLibraries.md | 8 +++- local-cli/core/__fixtures__/ios.js | 5 +++ local-cli/core/__fixtures__/projects.js | 8 +++- .../__tests__/ios/findPodfilePath.spec.js | 20 +++++++++ .../__tests__/ios/findPodspecName.spec.js | 44 +++++++++++++++++++ local-cli/core/ios/findPodfilePath.js | 11 +++++ local-cli/core/ios/findPodspecName.js | 28 ++++++++++++ local-cli/core/ios/index.js | 4 ++ .../link/__fixtures__/pods/PodfileSimple | 8 ++++ .../__fixtures__/pods/PodfileWithFunction | 30 +++++++++++++ .../link/__fixtures__/pods/PodfileWithMarkers | 34 ++++++++++++++ .../link/__fixtures__/pods/PodfileWithTarget | 32 ++++++++++++++ .../__tests__/pods/findLineToAddPod.spec.js | 30 +++++++++++++ .../pods/findMarkedLinesInPodfile.spec.js | 26 +++++++++++ .../__tests__/pods/findPodTargetLine.spec.js | 24 ++++++++++ .../link/__tests__/pods/isInstalled.spec.js | 32 ++++++++++++++ .../__tests__/pods/removePodEntry.spec.js | 35 +++++++++++++++ local-cli/link/link.js | 14 +++--- local-cli/link/pods/addPodEntry.js | 21 +++++++++ local-cli/link/pods/findLineToAddPod.js | 24 ++++++++++ .../link/pods/findMarkedLinesInPodfile.js | 12 +++++ local-cli/link/pods/findPodTargetLine.js | 14 ++++++ local-cli/link/pods/isInstalled.js | 19 ++++++++ local-cli/link/pods/readPodfile.js | 8 ++++ local-cli/link/pods/registerNativeModule.js | 25 +++++++++++ local-cli/link/pods/removePodEntry.js | 7 +++ local-cli/link/pods/savePodFile.js | 8 ++++ local-cli/link/pods/unregisterNativeModule.js | 13 ++++++ local-cli/link/unlink.js | 16 ++++--- 29 files changed, 547 insertions(+), 13 deletions(-) create mode 100644 local-cli/core/__tests__/ios/findPodfilePath.spec.js create mode 100644 local-cli/core/__tests__/ios/findPodspecName.spec.js create mode 100644 local-cli/core/ios/findPodfilePath.js create mode 100644 local-cli/core/ios/findPodspecName.js create mode 100644 local-cli/link/__fixtures__/pods/PodfileSimple create mode 100644 local-cli/link/__fixtures__/pods/PodfileWithFunction create mode 100644 local-cli/link/__fixtures__/pods/PodfileWithMarkers create mode 100644 local-cli/link/__fixtures__/pods/PodfileWithTarget create mode 100644 local-cli/link/__tests__/pods/findLineToAddPod.spec.js create mode 100644 local-cli/link/__tests__/pods/findMarkedLinesInPodfile.spec.js create mode 100644 local-cli/link/__tests__/pods/findPodTargetLine.spec.js create mode 100644 local-cli/link/__tests__/pods/isInstalled.spec.js create mode 100644 local-cli/link/__tests__/pods/removePodEntry.spec.js create mode 100644 local-cli/link/pods/addPodEntry.js create mode 100644 local-cli/link/pods/findLineToAddPod.js create mode 100644 local-cli/link/pods/findMarkedLinesInPodfile.js create mode 100644 local-cli/link/pods/findPodTargetLine.js create mode 100644 local-cli/link/pods/isInstalled.js create mode 100644 local-cli/link/pods/readPodfile.js create mode 100644 local-cli/link/pods/registerNativeModule.js create mode 100644 local-cli/link/pods/removePodEntry.js create mode 100644 local-cli/link/pods/savePodFile.js create mode 100644 local-cli/link/pods/unregisterNativeModule.js diff --git a/docs/LinkingLibraries.md b/docs/LinkingLibraries.md index 62390598ecb..ad15d09db6e 100644 --- a/docs/LinkingLibraries.md +++ b/docs/LinkingLibraries.md @@ -35,8 +35,8 @@ Install a library with native dependencies: $ npm install --save ``` -**Note:** _`--save` or `--save-dev` flag is very important for this step. React Native will link -your libs based on `dependencies` and `devDependencies` in your `package.json` file._ +> ***Note:*** `--save` or `--save-dev` flag is very important for this step. React Native will link +your libs based on `dependencies` and `devDependencies` in your `package.json` file. #### Step 2 @@ -47,6 +47,10 @@ $ react-native link Done! All libraries with native dependencies should be successfully linked to your iOS/Android project. +> ***Note:*** If your iOS project is using CocoaPods (contains `Podfile`) and linked library has `podspec` file, +then `react-native link` will link library using Podfile. To support non-trivial Podfiles +add `# Add new pods below this line` comment to places where you expect pods to be added. + ### Manual linking #### Step 1 diff --git a/local-cli/core/__fixtures__/ios.js b/local-cli/core/__fixtures__/ios.js index f2d1ee7f5bd..92256edcd61 100644 --- a/local-cli/core/__fixtures__/ios.js +++ b/local-cli/core/__fixtures__/ios.js @@ -5,6 +5,7 @@ exports.valid = { 'demoProject.xcodeproj': { 'project.pbxproj': fs.readFileSync(path.join(__dirname, './files/project.pbxproj')), }, + 'TestPod.podspec': 'empty' }; exports.validTestName = { @@ -12,3 +13,7 @@ exports.validTestName = { 'project.pbxproj': fs.readFileSync(path.join(__dirname, './files/project.pbxproj')), }, }; + +exports.pod = { + 'TestPod.podspec': 'empty' +}; diff --git a/local-cli/core/__fixtures__/projects.js b/local-cli/core/__fixtures__/projects.js index 8b004f5b98f..a9a74e06e02 100644 --- a/local-cli/core/__fixtures__/projects.js +++ b/local-cli/core/__fixtures__/projects.js @@ -4,6 +4,7 @@ const ios = require('./ios'); const flat = { android: android.valid, ios: ios.valid, + Podfile: 'empty' }; const nested = { @@ -19,4 +20,9 @@ const withExamples = { android: android.valid, }; -module.exports = { flat, nested, withExamples }; +const withPods = { + Podfile: 'content', + ios: ios.pod +}; + +module.exports = { flat, nested, withExamples, withPods }; diff --git a/local-cli/core/__tests__/ios/findPodfilePath.spec.js b/local-cli/core/__tests__/ios/findPodfilePath.spec.js new file mode 100644 index 00000000000..d184c8f6adf --- /dev/null +++ b/local-cli/core/__tests__/ios/findPodfilePath.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +jest.mock('fs'); + +const findPodfilePath = require('../../ios/findPodfilePath'); +const fs = require('fs'); +const projects = require('../../__fixtures__/projects'); +const ios = require('../../__fixtures__/ios'); + +describe('ios::findPodfilePath', () => { + it('returns null if there is no Podfile', () => { + fs.__setMockFilesystem(ios.valid); + expect(findPodfilePath('')).toBeNull(); + }); + + it('returns Podfile path if it exists', () => { + fs.__setMockFilesystem(projects.withPods); + expect(findPodfilePath('/ios')).toContain('Podfile'); + }); +}); diff --git a/local-cli/core/__tests__/ios/findPodspecName.spec.js b/local-cli/core/__tests__/ios/findPodspecName.spec.js new file mode 100644 index 00000000000..f42c19f3ea0 --- /dev/null +++ b/local-cli/core/__tests__/ios/findPodspecName.spec.js @@ -0,0 +1,44 @@ +'use strict'; + +jest.mock('fs'); + +const findPodspecName = require('../../ios/findPodspecName'); +const fs = require('fs'); +const projects = require('../../__fixtures__/projects'); +const ios = require('../../__fixtures__/ios'); + +describe('ios::findPodspecName', () => { + it('returns null if there is not podspec file', () => { + fs.__setMockFilesystem(projects.flat); + expect(findPodspecName('')).toBeNull(); + }); + + it('returns podspec name if only one exists', () => { + fs.__setMockFilesystem(ios.pod); + expect(findPodspecName('/')).toBe('TestPod'); + }); + + it('returns podspec name that match packet directory', () => { + fs.__setMockFilesystem({ + user: { + PacketName: { + 'Another.podspec': 'empty', + 'PacketName.podspec': 'empty' + } + } + }); + expect(findPodspecName('/user/PacketName')).toBe('PacketName'); + }); + + it('returns first podspec name if not match in directory', () => { + fs.__setMockFilesystem({ + user: { + packet: { + 'Another.podspec': 'empty', + 'PacketName.podspec': 'empty' + } + } + }); + expect(findPodspecName('/user/packet')).toBe('Another'); + }); +}); diff --git a/local-cli/core/ios/findPodfilePath.js b/local-cli/core/ios/findPodfilePath.js new file mode 100644 index 00000000000..6964a8d1ac7 --- /dev/null +++ b/local-cli/core/ios/findPodfilePath.js @@ -0,0 +1,11 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +module.exports = function findPodfilePath(projectFolder) { + const podFilePath = path.join(projectFolder, '..', 'Podfile'); + const podFileExists = fs.existsSync(podFilePath); + + return podFileExists ? podFilePath : null; +}; diff --git a/local-cli/core/ios/findPodspecName.js b/local-cli/core/ios/findPodspecName.js new file mode 100644 index 00000000000..53a7ca5c3f1 --- /dev/null +++ b/local-cli/core/ios/findPodspecName.js @@ -0,0 +1,28 @@ +'use strict'; + +const glob = require('glob'); +const path = require('path'); + +module.exports = function findPodspecName(folder) { + const podspecs = glob.sync('*.podspec', { cwd: folder }); + let podspecFile = null; + if (podspecs.length === 0) { + return null; + } + else if (podspecs.length === 1) { + podspecFile = podspecs[0]; + } + else { + const folderParts = folder.split(path.sep); + const currentFolder = folderParts[folderParts.length - 1]; + const toSelect = podspecs.indexOf(currentFolder + '.podspec'); + if (toSelect === -1) { + podspecFile = podspecs[0]; + } + else { + podspecFile = podspecs[toSelect]; + } + } + + return podspecFile.replace('.podspec', ''); +}; diff --git a/local-cli/core/ios/index.js b/local-cli/core/ios/index.js index 2cb18f62c2d..6bca5946352 100644 --- a/local-cli/core/ios/index.js +++ b/local-cli/core/ios/index.js @@ -9,6 +9,8 @@ 'use strict'; const findProject = require('./findProject'); +const findPodfilePath = require('./findPodfilePath'); +const findPodspecName = require('./findPodspecName'); const path = require('path'); /** @@ -44,6 +46,8 @@ exports.projectConfig = function projectConfigIOS(folder, userConfig) { sourceDir: path.dirname(projectPath), folder: folder, pbxprojPath: path.join(projectPath, 'project.pbxproj'), + podfile: findPodfilePath(projectPath), + podspec: findPodspecName(folder), projectPath: projectPath, projectName: path.basename(projectPath), libraryFolder: userConfig.libraryFolder || 'Libraries', diff --git a/local-cli/link/__fixtures__/pods/PodfileSimple b/local-cli/link/__fixtures__/pods/PodfileSimple new file mode 100644 index 00000000000..3e42c25278e --- /dev/null +++ b/local-cli/link/__fixtures__/pods/PodfileSimple @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' + +target 'Testing' do + pod 'TestPod', '~> 3.1' + + # test should point to this line +end diff --git a/local-cli/link/__fixtures__/pods/PodfileWithFunction b/local-cli/link/__fixtures__/pods/PodfileWithFunction new file mode 100644 index 00000000000..13b70fb0a90 --- /dev/null +++ b/local-cli/link/__fixtures__/pods/PodfileWithFunction @@ -0,0 +1,30 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' + +target 'none' do + pod 'React', + :path => "../node_modules/react-native", + :subspecs => [ + "Core", + "ART", + "RCTActionSheet", + "RCTAnimation", + "RCTCameraRoll", + "RCTGeolocation", + "RCTImage", + "RCTNetwork", + "RCTText", + "RCTVibration", + "RCTWebSocket", + "DevSupport", + "BatchedBridge" + ] + + pod 'Yoga', + :path => "../node_modules/react-native/ReactCommon/yoga" + + # test should point to this line + post_install do |installer| + + end +end diff --git a/local-cli/link/__fixtures__/pods/PodfileWithMarkers b/local-cli/link/__fixtures__/pods/PodfileWithMarkers new file mode 100644 index 00000000000..bf27d33689b --- /dev/null +++ b/local-cli/link/__fixtures__/pods/PodfileWithMarkers @@ -0,0 +1,34 @@ +source 'https://github.com/CocoaPods/Specs.git' +# platform :ios, '9.0' + +target 'None' do + # Uncomment the next line if you're using Swift or would like to use dynamic frameworks + # use_frameworks! + # Your 'node_modules' directory is probably in the root of your project, # but if not, adjust the `:path` accordingly + pod 'React', :path => '../node_modules/react-native', :subspecs => [ + 'Core', + 'RCTText', + 'RCTNetwork', + 'BatchedBridge', + 'RCTImage', + 'RCTWebSocket', # needed for debugging + # Add any other subspecs you want to use in your project + ] + + # Add new pods below this line + + # test should point to this line + target 'NoneTests' do + inherit! :search_paths + # Pods for testing + end +end + +target 'Second' do + + target 'NoneUITests' do + inherit! :search_paths + # Add new pods below this line + end + +end \ No newline at end of file diff --git a/local-cli/link/__fixtures__/pods/PodfileWithTarget b/local-cli/link/__fixtures__/pods/PodfileWithTarget new file mode 100644 index 00000000000..5887ababea7 --- /dev/null +++ b/local-cli/link/__fixtures__/pods/PodfileWithTarget @@ -0,0 +1,32 @@ +source 'https://github.com/CocoaPods/Specs.git' +# platform :ios, '9.0' + +target 'None' do + # Uncomment the next line if you're using Swift or would like to use dynamic frameworks + # use_frameworks! + # Your 'node_modules' directory is probably in the root of your project, # but if not, adjust the `:path` accordingly + pod 'React', :path => '../node_modules/react-native', :subspecs => [ + 'Core', + 'RCTText', + 'RCTNetwork', + 'BatchedBridge', + 'RCTImage', + 'RCTWebSocket', # needed for debugging + # Add any other subspecs you want to use in your project + ] + + # Explicitly include Yoga if you are using RN >= 0.42.0 + pod "Yoga", :path => "../node_modules/react-native/ReactCommon/yoga" + + # test should point to this line + target 'NoneTests' do + inherit! :search_paths + # Pods for testing + end + + target 'NoneUITests' do + inherit! :search_paths + # Pods for testing + end + +end \ No newline at end of file diff --git a/local-cli/link/__tests__/pods/findLineToAddPod.spec.js b/local-cli/link/__tests__/pods/findLineToAddPod.spec.js new file mode 100644 index 00000000000..80411590a43 --- /dev/null +++ b/local-cli/link/__tests__/pods/findLineToAddPod.spec.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); +const findLineToAddPod = require('../../pods/findLineToAddPod'); +const readPodfile = require('../../pods/readPodfile'); + +const PODFILES_PATH = path.join(__dirname, '../../__fixtures__/pods'); +const LINE_AFTER_TARGET_IN_TEST_PODFILE = 4; + +describe('pods::findLineToAddPod', () => { + it('returns null if file is not Podfile', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, '../Info.plist')); + expect(findLineToAddPod(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toBeNull(); + }); + + it('returns correct line number for Simple Podfile', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileSimple')); + expect(findLineToAddPod(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toEqual({ line: 7, indentation: 2 }); + }); + + it('returns correct line number for Podfile with target', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileWithTarget')); + expect(findLineToAddPod(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toEqual({ line: 21, indentation: 2 }); + }); + + it('returns correct line number for Podfile with function', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileWithFunction')); + expect(findLineToAddPod(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toEqual({ line: 26, indentation: 2 }); + }); +}); diff --git a/local-cli/link/__tests__/pods/findMarkedLinesInPodfile.spec.js b/local-cli/link/__tests__/pods/findMarkedLinesInPodfile.spec.js new file mode 100644 index 00000000000..55e711cbd67 --- /dev/null +++ b/local-cli/link/__tests__/pods/findMarkedLinesInPodfile.spec.js @@ -0,0 +1,26 @@ +'use strict'; + +const path = require('path'); +const readPodfile = require('../../pods/readPodfile'); +const findMarkedLinesInPodfile = require('../../pods/findMarkedLinesInPodfile'); + +const PODFILES_PATH = path.join(__dirname, '../../__fixtures__/pods'); +const LINE_AFTER_TARGET_IN_TEST_PODFILE = 4; + +describe('pods::findMarkedLinesInPodfile', () => { + it('returns empty array if file is not Podfile', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, '../Info.plist')); + expect(findMarkedLinesInPodfile(podfile)).toEqual([]); + }); + + it('returns empty array for Simple Podfile', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileSimple')); + expect(findMarkedLinesInPodfile(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toEqual([]); + }); + + it('returns correct line numbers for Podfile with marker', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileWithMarkers')); + const expectedObject = [{ line: 18, indentation: 2 }, { line: 31, indentation: 4 }]; + expect(findMarkedLinesInPodfile(podfile, LINE_AFTER_TARGET_IN_TEST_PODFILE)).toEqual(expectedObject); + }); +}); diff --git a/local-cli/link/__tests__/pods/findPodTargetLine.spec.js b/local-cli/link/__tests__/pods/findPodTargetLine.spec.js new file mode 100644 index 00000000000..6bc54b6586d --- /dev/null +++ b/local-cli/link/__tests__/pods/findPodTargetLine.spec.js @@ -0,0 +1,24 @@ +'use strict'; + +const path = require('path'); +const findPodTargetLine = require('../../pods/findPodTargetLine'); +const readPodfile = require('../../pods/readPodfile'); + +const PODFILES_PATH = path.join(__dirname, '../../__fixtures__/pods'); + +describe('pods::findPodTargetLine', () => { + it('returns null if file is not Podfile', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, '../Info.plist')); + expect(findPodTargetLine(podfile, 'name')).toBeNull(); + }); + + it('returns null if there is not matching project name', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileSimple')); + expect(findPodTargetLine(podfile, 'invalidName')).toBeNull(); + }); + + it('returns null if there is not matching project name', () => { + const podfile = readPodfile(path.join(PODFILES_PATH, 'PodfileSimple')); + expect(findPodTargetLine(podfile, 'Testing')).toBe(4); + }); +}); diff --git a/local-cli/link/__tests__/pods/isInstalled.spec.js b/local-cli/link/__tests__/pods/isInstalled.spec.js new file mode 100644 index 00000000000..b4f30793123 --- /dev/null +++ b/local-cli/link/__tests__/pods/isInstalled.spec.js @@ -0,0 +1,32 @@ +'use strict'; + +const path = require('path'); +const isInstalled = require('../../pods/isInstalled'); + +const PODFILES_PATH = path.join(__dirname, '../../__fixtures__/pods'); + +describe('pods::isInstalled', () => { + it('returns false if pod is missing', () => { + const project = { podfile: path.join(PODFILES_PATH, 'PodfileSimple') }; + const podspecName = { podspec: 'NotExisting' }; + expect(isInstalled(project, podspecName)).toBe(false); + }); + + it('returns true for existing pod with version number', () => { + const project = { podfile: path.join(PODFILES_PATH, 'PodfileSimple') }; + const podspecName = { podspec: 'TestPod' }; + expect(isInstalled(project, podspecName)).toBe(true); + }); + + it('returns true for existing pod with path', () => { + const project = { podfile: path.join(PODFILES_PATH, 'PodfileWithTarget') }; + const podspecName = { podspec: 'Yoga' }; + expect(isInstalled(project, podspecName)).toBe(true); + }); + + it('returns true for existing pod with multiline definition', () => { + const project = { podfile: path.join(PODFILES_PATH, 'PodfileWithFunction') }; + const podspecName = { podspec: 'React' }; + expect(isInstalled(project, podspecName)).toBe(true); + }); +}); diff --git a/local-cli/link/__tests__/pods/removePodEntry.spec.js b/local-cli/link/__tests__/pods/removePodEntry.spec.js new file mode 100644 index 00000000000..e6fd1a09b70 --- /dev/null +++ b/local-cli/link/__tests__/pods/removePodEntry.spec.js @@ -0,0 +1,35 @@ +'use strict'; + +const path = require('path'); +const removePodEntry = require('../../pods/removePodEntry'); +const readPodfile = require('../../pods/readPodfile'); + +const PODFILES_PATH = path.join(__dirname, '../../__fixtures__/pods'); + +describe('pods::removePodEntry', () => { + it('should remove one line from Podfile with TestPod', () => { + const { podfileContent, podLinesCount } = readTestPodFile('PodfileSimple'); + const podFileWithRemoved = removePodEntry(podfileContent, 'TestPod'); + const newLineCount = podFileWithRemoved.split('\n').length; + expect(newLineCount).toBe(podLinesCount - 1); + }); + + it('should remove one line from Podfile with Yoga', () => { + const { podfileContent, podLinesCount } = readTestPodFile('PodfileWithTarget'); + const podFileWithRemoved = removePodEntry(podfileContent, 'Yoga'); + const newLineCount = podFileWithRemoved.split('\n').length; + expect(newLineCount).toBe(podLinesCount - 1); + }); + + it('should remove whole reference to React pod from Podfile', () => { + const { podfileContent, podLinesCount } = readTestPodFile('PodfileWithTarget'); + const podFileWithRemoved = removePodEntry(podfileContent, 'React'); + const newLineCount = podFileWithRemoved.split('\n').length; + expect(newLineCount).toBe(podLinesCount - 9); + }); +}); + +function readTestPodFile(fileName) { + const podfileLines = readPodfile(path.join(PODFILES_PATH, fileName)); + return { podfileContent: podfileLines.join('\n'), podLinesCount: podfileLines.length }; +} diff --git a/local-cli/link/link.js b/local-cli/link/link.js index e20f13b4a50..969bf63a86b 100644 --- a/local-cli/link/link.js +++ b/local-cli/link/link.js @@ -29,9 +29,11 @@ const promiseWaterfall = require('./promiseWaterfall'); const registerDependencyAndroid = require('./android/registerNativeModule'); const registerDependencyWindows = require('./windows/registerNativeModule'); const registerDependencyIOS = require('./ios/registerNativeModule'); +const registerDependencyPods = require('./pods/registerNativeModule'); const isInstalledAndroid = require('./android/isInstalled'); const isInstalledWindows = require('./windows/isInstalled'); const isInstalledIOS = require('./ios/isInstalled'); +const isInstalledPods = require('./pods/isInstalled'); const copyAssetsAndroid = require('./android/copyAssets'); const copyAssetsIOS = require('./ios/copyAssets'); const getProjectDependencies = require('./getProjectDependencies'); @@ -106,17 +108,19 @@ const linkDependencyIOS = (iOSProject, dependency) => { return; } - const isInstalled = isInstalledIOS(iOSProject, dependency.config.ios); - + const isInstalled = isInstalledIOS(iOSProject, dependency.config.ios) || isInstalledPods(iOSProject, dependency.config.ios); if (isInstalled) { log.info(chalk.grey(`iOS module ${dependency.name} is already linked`)); return; } log.info(`Linking ${dependency.name} ios dependency`); - - registerDependencyIOS(dependency.config.ios, iOSProject); - + if (iOSProject.podfile && dependency.config.ios.podspec) { + registerDependencyPods(dependency, iOSProject); + } + else { + registerDependencyIOS(dependency.config.ios, iOSProject); + } log.info(`iOS module ${dependency.name} has been successfully linked`); }; diff --git a/local-cli/link/pods/addPodEntry.js b/local-cli/link/pods/addPodEntry.js new file mode 100644 index 00000000000..2316f260ac4 --- /dev/null +++ b/local-cli/link/pods/addPodEntry.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = function addPodEntry(podLines, linesToAddEntry, podName, nodePath) { + const newEntry = `pod '${podName}', :path => '../node_modules/${nodePath}'\n`; + + if (!linesToAddEntry) { + return; + } else if (Array.isArray(linesToAddEntry)) { + linesToAddEntry.map(({ line, indentation }, idx) => + podLines.splice(line + idx, 0, getLineToAdd(newEntry, indentation)) + ); + } else { + const { line, indentation } = linesToAddEntry; + podLines.splice(line, 0, getLineToAdd(newEntry, indentation)); + } +}; + +function getLineToAdd(newEntry, indentation) { + const spaces = Array(indentation + 1).join(' '); + return spaces + newEntry; +} diff --git a/local-cli/link/pods/findLineToAddPod.js b/local-cli/link/pods/findLineToAddPod.js new file mode 100644 index 00000000000..902194f7e36 --- /dev/null +++ b/local-cli/link/pods/findLineToAddPod.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = function findLineToAddPod(podLines, firstTargetLine) { + // match line with new target: target 'project_name' do (most likely target inside podfile main target) + const nextTarget = /target (\'|\")\w+(\'|\") do/g; + // match line that has only 'end' (if we don't catch new target or function, this would mean this is end of current target) + const endOfCurrentTarget = /^\s*end\s*$/g; + // match function definition, like: post_install do |installer| (some Podfiles have function defined inside main target + const functionDefinition = /^\s*[a-z_]+\s+do(\s+\|[a-z]+\|)?/g; + + for (let i = firstTargetLine, len = podLines.length; i < len; i++) { + const matchNextConstruct = podLines[i].match(nextTarget) || podLines[i].match(functionDefinition); + const matchEnd = podLines[i].match(endOfCurrentTarget); + + if (matchNextConstruct || matchEnd) { + const firstNonSpaceCharacter = podLines[i].search(/\S/); + return { + indentation: firstNonSpaceCharacter + (matchEnd ? 2 : 0), + line: i + }; + } + } + return null; +}; diff --git a/local-cli/link/pods/findMarkedLinesInPodfile.js b/local-cli/link/pods/findMarkedLinesInPodfile.js new file mode 100644 index 00000000000..2154cd5227c --- /dev/null +++ b/local-cli/link/pods/findMarkedLinesInPodfile.js @@ -0,0 +1,12 @@ +'use strict'; +const MARKER_TEXT = '# Add new pods below this line'; + +module.exports = function findMarkedLinesInPodfile(podLines) { + const result = []; + for (let i = 0, len = podLines.length; i < len; i++) { + if (podLines[i].includes(MARKER_TEXT)) { + result.push({ line: i + 1, indentation: podLines[i].indexOf('#') }); + } + } + return result; +}; diff --git a/local-cli/link/pods/findPodTargetLine.js b/local-cli/link/pods/findPodTargetLine.js new file mode 100644 index 00000000000..bc4e6f85e1d --- /dev/null +++ b/local-cli/link/pods/findPodTargetLine.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = function findPodTargetLine(podLines, projectName) { + const targetName = projectName.replace('.xcodeproj', ''); + //match first target definition in file: target 'target_name' do + const targetRegex = new RegExp('target (\'|\")' + targetName + '(\'|\") do', 'g'); + for (let i = 0, len = podLines.length; i < len; i++) { + const match = podLines[i].match(targetRegex); + if (match) { + return i + 1; + } + } + return null; +}; diff --git a/local-cli/link/pods/isInstalled.js b/local-cli/link/pods/isInstalled.js new file mode 100644 index 00000000000..55100275be1 --- /dev/null +++ b/local-cli/link/pods/isInstalled.js @@ -0,0 +1,19 @@ +'use strict'; + +const readPodfile = require('./readPodfile'); + +module.exports = function isInstalled(iOSProject, dependencyConfig) { + if (!iOSProject.podfile) { + return false; + } + // match line with pod declaration: pod 'dependencyPodName' (other possible parameters of pod are ignored) + const dependencyRegExp = new RegExp('pod\\s+(\'|\")' + dependencyConfig.podspec + '(\'|\")', 'g'); + const podLines = readPodfile(iOSProject.podfile); + for (let i = 0, len = podLines.length; i < len; i++) { + const match = podLines[i].match(dependencyRegExp); + if (match) { + return true; + } + } + return false; +}; diff --git a/local-cli/link/pods/readPodfile.js b/local-cli/link/pods/readPodfile.js new file mode 100644 index 00000000000..72613b7f04f --- /dev/null +++ b/local-cli/link/pods/readPodfile.js @@ -0,0 +1,8 @@ +'use strict'; + +const fs = require('fs'); + +module.exports = function readPodfile(podfilePath) { + const podContent = fs.readFileSync(podfilePath, 'utf8'); + return podContent.split(/\r?\n/g); +}; diff --git a/local-cli/link/pods/registerNativeModule.js b/local-cli/link/pods/registerNativeModule.js new file mode 100644 index 00000000000..e29c5642a1c --- /dev/null +++ b/local-cli/link/pods/registerNativeModule.js @@ -0,0 +1,25 @@ +'use strict'; + +const readPodfile = require('./readPodfile'); +const findPodTargetLine = require('./findPodTargetLine'); +const findLineToAddPod = require('./findLineToAddPod'); +const findMarkedLinesInPodfile = require('./findMarkedLinesInPodfile'); +const addPodEntry = require('./addPodEntry'); +const savePodFile = require('./savePodFile'); + +module.exports = function registerNativeModulePods(dependency, iOSProject) { + const podLines = readPodfile(iOSProject.podfile); + const linesToAddEntry = getLinesToAddEntry(podLines, iOSProject); + addPodEntry(podLines, linesToAddEntry, dependency.config.ios.podspec, dependency.name); + savePodFile(iOSProject.podfile, podLines); +}; + +function getLinesToAddEntry(podLines, { projectName }) { + const linesToAddPodWithMarker = findMarkedLinesInPodfile(podLines); + if (linesToAddPodWithMarker.length > 0) { + return linesToAddPodWithMarker; + } else { + const firstTargetLined = findPodTargetLine(podLines, projectName); + return findLineToAddPod(podLines, firstTargetLined); + } +} diff --git a/local-cli/link/pods/removePodEntry.js b/local-cli/link/pods/removePodEntry.js new file mode 100644 index 00000000000..6c6d28887c0 --- /dev/null +++ b/local-cli/link/pods/removePodEntry.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function removePodEntry(podfileContent, podName) { + // this regex should catch line(s) with full pod definition, like: pod 'podname', :path => '../node_modules/podname', :subspecs => ['Sub2', 'Sub1'] + const podRegex = new RegExp("\\n( |\\t)*pod\\s+(\"|')" + podName + "(\"|')(,\\s*(:[a-z]+\\s*=>)?\\s*((\"|').*?(\"|')|\\[[\\s\\S]*?\\]))*\\n", 'g'); + return podfileContent.replace(podRegex, '\n'); +}; diff --git a/local-cli/link/pods/savePodFile.js b/local-cli/link/pods/savePodFile.js new file mode 100644 index 00000000000..b615c9a541d --- /dev/null +++ b/local-cli/link/pods/savePodFile.js @@ -0,0 +1,8 @@ +'use strict'; + +const fs = require('fs'); + +module.exports = function savePodFile(podfilePath, podLines) { + const newPodfile = podLines.join('\n'); + fs.writeFileSync(podfilePath, newPodfile); +}; diff --git a/local-cli/link/pods/unregisterNativeModule.js b/local-cli/link/pods/unregisterNativeModule.js new file mode 100644 index 00000000000..b111eb836f9 --- /dev/null +++ b/local-cli/link/pods/unregisterNativeModule.js @@ -0,0 +1,13 @@ +'use strict'; + +const fs = require('fs'); +const removePodEntry = require('./removePodEntry'); + +/** + * Unregister native module IOS with CocoaPods + */ +module.exports = function unregisterNativeModule(dependencyConfig, iOSProject) { + const podContent = fs.readFileSync(iOSProject.podfile, 'utf8'); + const removed = removePodEntry(podContent, dependencyConfig.podspec); + fs.writeFileSync(iOSProject.podfile, removed); +}; diff --git a/local-cli/link/unlink.js b/local-cli/link/unlink.js index 0ac7c8b8a03..af78ff7eaf4 100644 --- a/local-cli/link/unlink.js +++ b/local-cli/link/unlink.js @@ -4,16 +4,17 @@ const getProjectDependencies = require('./getProjectDependencies'); const unregisterDependencyAndroid = require('./android/unregisterNativeModule'); const unregisterDependencyWindows = require('./windows/unregisterNativeModule'); const unregisterDependencyIOS = require('./ios/unregisterNativeModule'); +const unregisterDependencyPods = require('./pods/unregisterNativeModule'); const isInstalledAndroid = require('./android/isInstalled'); const isInstalledWindows = require('./windows/isInstalled'); const isInstalledIOS = require('./ios/isInstalled'); +const isInstalledPods = require('./pods/isInstalled'); const unlinkAssetsAndroid = require('./android/unlinkAssets'); const unlinkAssetsIOS = require('./ios/unlinkAssets'); const getDependencyConfig = require('./getDependencyConfig'); const compact = require('lodash').compact; const difference = require('lodash').difference; const filter = require('lodash').filter; -const find = require('lodash').find; const flatten = require('lodash').flatten; const isEmpty = require('lodash').isEmpty; const promiseWaterfall = require('./promiseWaterfall'); @@ -65,16 +66,21 @@ const unlinkDependencyIOS = (iOSProject, dependency, packageName, iOSDependencie return; } - const isInstalled = isInstalledIOS(iOSProject, dependency.ios); - - if (!isInstalled) { + const isIosInstalled = isInstalledIOS(iOSProject, dependency.ios); + const isPodInstalled = isInstalledPods(iOSProject, dependency.ios); + if (!isIosInstalled && !isPodInstalled) { log.info(`iOS module ${packageName} is not installed`); return; } log.info(`Unlinking ${packageName} ios dependency`); - unregisterDependencyIOS(dependency.ios, iOSProject, iOSDependencies); + if (isIosInstalled) { + unregisterDependencyIOS(dependency.ios, iOSProject, iOSDependencies); + } + else if (isPodInstalled) { + unregisterDependencyPods(dependency.ios, iOSProject); + } log.info(`iOS module ${packageName} has been successfully unlinked`); };