diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bd31c04e..a1d7dfa49 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: diff --git a/package.json b/package.json index 9d1cb7ac7..fe4ddc887 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap index 02aeb3db5..7c5496b7f 100644 --- a/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap +++ b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.js.snap @@ -80,6 +80,7 @@ Object { "sourceDir": "./abc", }, }, + "root": "<>/node_modules/react-native-test", } `; @@ -113,6 +114,7 @@ Object { "sourceDir": "<>/node_modules/react-native-foo/customLocation", }, }, + "root": "<>/node_modules/react-native-foo", } `; @@ -130,13 +132,14 @@ Object { "pbxprojPath": "<>/node_modules/react-native-test/customLocation/customProject.xcodeproj/project.pbxproj", "plist": Array [], "podfile": null, - "podspec": null, + "podspec": "ReactNativeTest", "projectName": "customProject.xcodeproj", "projectPath": "<>/node_modules/react-native-test/customLocation/customProject.xcodeproj", "sharedLibraries": Array [], "sourceDir": "<>/node_modules/react-native-test/customLocation", }, }, + "root": "<>/node_modules/react-native-test", } `; @@ -151,6 +154,7 @@ Object { "android": null, "ios": null, }, + "root": "<>/node_modules/react-native", }, "react-native-test": Object { "assets": Array [], @@ -172,6 +176,7 @@ Object { "sourceDir": "<>/node_modules/react-native-test/ios", }, }, + "root": "<>/node_modules/react-native-test", }, } `; diff --git a/packages/cli/src/tools/config/__tests__/index-test.js b/packages/cli/src/tools/config/__tests__/index-test.js index 8a58dd9b7..117cb393a 100644 --- a/packages/cli/src/tools/config/__tests__/index-test.js +++ b/packages/cli/src/tools/config/__tests__/index-test.js @@ -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": { diff --git a/packages/cli/src/tools/config/index.js b/packages/cli/src/tools/config/index.js index 640d4d13b..5acc1775e 100644 --- a/packages/cli/src/tools/config/index.js +++ b/packages/cli/src/tools/config/index.js @@ -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. */ @@ -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) => { diff --git a/packages/cli/src/tools/config/readConfigFromDisk.js b/packages/cli/src/tools/config/readConfigFromDisk.js index 121937e2b..6223eab3f 100644 --- a/packages/cli/src/tools/config/readConfigFromDisk.js +++ b/packages/cli/src/tools/config/readConfigFromDisk.js @@ -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, diff --git a/packages/cli/src/tools/config/types.flow.js b/packages/cli/src/tools/config/types.flow.js index ec59790c5..7f8d82a5c 100644 --- a/packages/cli/src/tools/config/types.flow.js +++ b/packages/cli/src/tools/config/types.flow.js @@ -108,6 +108,7 @@ export type ConfigT = {| dependencies: { [key: string]: { name: string, + root: string, platforms: { android?: DependencyConfigAndroidT | null, ios?: DependencyConfigIOST | null, diff --git a/packages/platform-ios/native_modules.rb b/packages/platform-ios/native_modules.rb new file mode 100644 index 000000000..287468f95 --- /dev/null +++ b/packages/platform-ios/native_modules.rb @@ -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