Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: autolinking for iOS with CocoaPods #256

2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2

defaults: &defaults
docker:
- image: circleci/node:8
- image: circleci/ruby:2.4-node
working_directory: ~/react-native-cli

jobs:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"build": "node ./scripts/build.js",
"build-clean": "rm -rf ./packages/*/build",
"watch": "node ./scripts/watch.js",
"test": "jest",
"test": "jest && yarn test:cocoapods-native-modules",
"test:ci:unit": "jest packages --ci --coverage",
"test:ci:e2e": "jest e2e --ci -i",
"test:cocoapods-native-modules": "ruby packages/platform-ios/native_modules.rb",
"lint": "eslint . --cache --report-unused-disable-directives",
"flow-check": "flow check",
"postinstall": "yarn build",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Object {
"sourceDir": "./abc",
},
},
"root": "<<REPLACED>>/node_modules/react-native-test",
}
`;

Expand Down Expand Up @@ -113,6 +114,7 @@ Object {
"sourceDir": "<<REPLACED>>/node_modules/react-native-foo/customLocation",
},
},
"root": "<<REPLACED>>/node_modules/react-native-foo",
}
`;

Expand All @@ -130,13 +132,14 @@ Object {
"pbxprojPath": "<<REPLACED>>/node_modules/react-native-test/customLocation/customProject.xcodeproj/project.pbxproj",
"plist": Array [],
"podfile": null,
"podspec": null,
"podspec": "ReactNativeTest",
"projectName": "customProject.xcodeproj",
"projectPath": "<<REPLACED>>/node_modules/react-native-test/customLocation/customProject.xcodeproj",
"sharedLibraries": Array [],
"sourceDir": "<<REPLACED>>/node_modules/react-native-test/customLocation",
},
},
"root": "<<REPLACED>>/node_modules/react-native-test",
}
`;

Expand All @@ -151,6 +154,7 @@ Object {
"android": null,
"ios": null,
},
"root": "<<REPLACED>>/node_modules/react-native",
},
"react-native-test": Object {
"assets": Array [],
Expand All @@ -172,6 +176,7 @@ Object {
"sourceDir": "<<REPLACED>>/node_modules/react-native-test/ios",
},
},
"root": "<<REPLACED>>/node_modules/react-native-test",
},
}
`;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/tools/config/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ test('should return dependencies from package.json', () => {
test('should read a config of a dependency and use it to load other settings', () => {
writeFiles(DIR, {
'node_modules/react-native/package.json': '{}',
'node_modules/react-native-test/ReactNativeTest.podspec': '',
'node_modules/react-native-test/package.json': `{
"react-native": {
"dependency": {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/tools/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function loadConfig(projectRoot: string = process.cwd()): ConfigT {
}

/**
* This workaround is neccessary for development only before
* This workaround is necessary for development only before
* first 0.60.0-rc.0 gets released and we can switch to it
* while testing.
*/
Expand All @@ -76,6 +76,7 @@ function loadConfig(projectRoot: string = process.cwd()): ConfigT {
get [dependencyName]() {
return merge(
{
root,
name: dependencyName,
platforms: Object.keys(finalConfig.platforms).reduce(
(dependency, platform) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/tools/config/readConfigFromDisk.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const loadProjectCommands = (
};

/**
* Reads a legacy configuaration from a `package.json` "rnpm" key.
* Reads a legacy configuration from a `package.json` "rnpm" key.
*/
export function readLegacyDependencyConfigFromDisk(
rootFolder: string,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/tools/config/types.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export type ConfigT = {|
dependencies: {
[key: string]: {
name: string,
root: string,
platforms: {
android?: DependencyConfigAndroidT | null,
ios?: DependencyConfigIOST | null,
Expand Down
237 changes: 237 additions & 0 deletions packages/platform-ios/native_modules.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
def use_native_modules!(packages = nil)
if (!packages)
cli_bin = Pod::Executable.execute_command("node", ["-e", "console.log(require.resolve('@react-native-community/cli/build/index.js'))"], true).strip
output = Pod::Executable.execute_command("node", [cli_bin, "config"], true)
json = []
output.each_line do |line|
case line
when /^warn\s(.+)/
Pod::UI.warn($1)
when /^(success|info|error|debug)\s(.+)/
Pod::UI.message($1)
else
json << line
end
end
config = JSON.parse(json.join("\n"))
packages = config["dependencies"]
end

found_pods = []

packages.each do |package_name, package|
next unless package_config = package["platforms"]["ios"]

podspec_path = File.join(package["root"], "#{package_config["podspec"]}.podspec")
spec = Pod::Specification.from_file(podspec_path)

# We want to do a look up inside the current CocoaPods target
# to see if it's already included, this:
# 1. Gives you the chance to define it beforehand
# 2. Ensures CocoaPods won't explode if it's included twice
#
this_target = current_target_definition
existing_deps = current_target_definition.dependencies

# Skip dependencies that the user already activated themselves.
next if existing_deps.find do |existing_dep|
existing_dep.name.split('/').first == spec.name
end

pod spec.name, :path => package["root"]

if package_config["scriptPhases"]
# Can be either an object, or an array of objects
Array(package_config["scriptPhases"]).each do |phase|
# see https://www.rubydoc.info/gems/cocoapods-core/Pod/Podfile/DSL#script_phase-instance_method
# for the full object keys

# Support passing in a path relative to the root of the package
if phase["path"]
phase["script"] = File.read(File.expand_path(phase["path"], package["root"]))
phase.delete("path")
end

# Support converting the execution position into a symbol
if phase["execution_position"]
phase["execution_position"] = phase["execution_position"].to_sym
end

script_phase phase
end
end

found_pods.push spec
end

if found_pods.size > 0
pods = found_pods.map { |p| p.name }.sort.to_sentence
Pod::UI.puts "Detected React Native module #{"pod".pluralize(found_pods.size)} for #{pods}"
end
end

# You can run the tests for this file by running:
# $ ruby use_native_modules.rb
if $0 == __FILE__
require "minitest/spec"
require "minitest/autorun"

# Define this here, because we’re not actually loading this code.
module Pod
class Specification
end

module UI
end
end

# CocoaPods loads ActiveSupport, but we’re not doing that here just for the test.
class Array
def to_sentence
size == 1 ? self[0] : "#{self[0..-2].join(", ")}, and #{self[-1]}"
end
end
class String
def pluralize(count)
count == 1 ? self : "#{self}s"
end
end

describe "use_native_modules!" do
before do
@script_phase = {
"script" => "123",
"name" => "My Name",
"execution_position" => "before_compile",
"input" => "string"
}

@ios_package = ios_package = {
"root" => "/Users/grabbou/Repositories/WebViewDemoApp/node_modules/react",
"platforms" => {
"ios" => {
"podspec" => "React",
},
"android" => nil,
},
}
@android_package = {
"root" => "/Users/grabbou/Repositories/WebViewDemoApp/node_modules/react-native-google-play-game-services",
"platforms" => {
"ios" => nil,
"android" => {
# This is where normally more config would be
},
}
}
@config = { "ios-dep" => @ios_package, "android-dep" => @android_package }

@activated_pods = activated_pods = []
@current_target_definition_dependencies = current_target_definition_dependencies = []
@printed_messages = printed_messages = []
@added_scripts = added_scripts = []
@target_definition = target_definition = Object.new
@podfile = podfile = Object.new
@spec = spec = Object.new

spec.singleton_class.send(:define_method, :name) { "ios-dep" }

podfile.singleton_class.send(:define_method, :use_native_modules) do |config|
use_native_modules!(config)
end

Pod::Specification.singleton_class.send(:define_method, :from_file) do |podspec_path|
podspec_path.must_equal File.join(ios_package["root"], "#{ios_package["platforms"]["ios"]["podspec"]}.podspec")
spec
end

Pod::UI.singleton_class.send(:define_method, :puts) do |message|
printed_messages << message
end

podfile.singleton_class.send(:define_method, :pod) do |name, options|
activated_pods << { name: name, options: options }
end

podfile.singleton_class.send(:define_method, :script_phase) do |options|
added_scripts << options
end

target_definition.singleton_class.send(:define_method, :dependencies) do
current_target_definition_dependencies
end

podfile.singleton_class.send(:define_method, :current_target_definition) do
target_definition
end
end

it "activates iOS pods" do
@podfile.use_native_modules(@config)
@activated_pods.must_equal [{
name: "ios-dep",
options: { path: @ios_package["root"] }
}]
end

it "does not activate pods that were already activated previously (by the user in their Podfile)" do
activated_pod = Object.new
activated_pod.singleton_class.send(:define_method, :name) { "ios-dep" }
@current_target_definition_dependencies << activated_pod
@podfile.use_native_modules(@config)
@activated_pods.must_equal []
end

it "does not activate pods whose root spec were already activated previously (by the user in their Podfile)" do
activated_pod = Object.new
activated_pod.singleton_class.send(:define_method, :name) { "ios-dep/foo/bar" }
@current_target_definition_dependencies << activated_pod
@podfile.use_native_modules(@config)
@activated_pods.must_equal []
end

it "prints out the native module pods that were found" do
@podfile.use_native_modules({})
@podfile.use_native_modules({ "pkg-1" => @ios_package })
@podfile.use_native_modules({ "pkg-1" => @ios_package, "pkg-2" => @ios_package })
@printed_messages.must_equal [
"Detected native module pod for ios-dep",
"Detected native module pods for ios-dep, and ios-dep"
]
end

describe "concerning script_phases" do
it "uses the options directly" do
@config["ios-dep"]["platforms"]["ios"]["scriptPhases"] = [@script_phase]
@podfile.use_native_modules(@config)
@added_scripts.must_equal [{
"script" => "123",
"name" => "My Name",
"execution_position" => :before_compile,
"input" => "string"
}]
end

it "reads a script file relative to the package root" do
@script_phase.delete("script")
@script_phase["path"] = "./some_shell_script.sh"
@config["ios-dep"]["platforms"]["ios"]["scriptPhases"] = [@script_phase]

file_read_mock = MiniTest::Mock.new
file_read_mock.expect(:call, "contents from file", [File.join(@ios_package["root"], "some_shell_script.sh")])

File.stub(:read, file_read_mock) do
@podfile.use_native_modules(@config)
end

@added_scripts.must_equal [{
"script" => "contents from file",
"name" => "My Name",
"execution_position" => :before_compile,
"input" => "string"
}]
file_read_mock.verify
end
end
end
end