Class: ReactNativeUtil::Converter

Inherits:
Object
  • Object
show all
Includes:
Util
Defined in:
lib/react_native_util/converter.rb

Overview

Class to perform conversion operations.

Constant Summary collapse

PODFILE_TEMPLATE_PATH =
String

Path to the Podfile template

File.expand_path '../assets/templates/Podfile.erb', __dir__
REQUIRED_COMMANDS =
[:yarn, 'react-native' => 'react-native-cli']

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Util

#boolean_env_var?, #elapsed_from, #execute, #float_env_var, #have_command?, #log, #mac?, #platform, #run_command_with_spinner!, #validate_commands!

Constructor Details

#initialize(repo_update: nil) ⇒ Converter



31
32
33
34
35
36
37
38
# File 'lib/react_native_util/converter.rb', line 31

def initialize(repo_update: nil)
  @options = {}
  if repo_update.nil?
    @options[:repo_update] = boolean_env_var?(:REACT_NATIVE_UTIL_REPO_UPDATE, default_value: true)
  else
    @options[:repo_update] = repo_update
  end
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



27
28
29
# File 'lib/react_native_util/converter.rb', line 27

def options
  @options
end

#package_jsonObject (readonly)

Hash

Contents of ./package.json



19
20
21
# File 'lib/react_native_util/converter.rb', line 19

def package_json
  @package_json
end

#projectObject (readonly)

Project

Contents of the project at xcodeproj_path



25
26
27
# File 'lib/react_native_util/converter.rb', line 25

def project
  @project
end

#react_podspecObject (readonly)

Returns the value of attribute react_podspec.



29
30
31
# File 'lib/react_native_util/converter.rb', line 29

def react_podspec
  @react_podspec
end

#react_projectObject (readonly)

Returns the value of attribute react_project.



28
29
30
# File 'lib/react_native_util/converter.rb', line 28

def react_project
  @react_project
end

#xcodeproj_pathObject (readonly)

String

Full path to Xcode project



22
23
24
# File 'lib/react_native_util/converter.rb', line 22

def xcodeproj_path
  @xcodeproj_path
end

Instance Method Details

#app_nameString

The name of the app as specified in package.json



296
297
298
# File 'lib/react_native_util/converter.rb', line 296

def app_name
  @app_name ||= package_json['name']
end

#app_targetObject



300
301
302
# File 'lib/react_native_util/converter.rb', line 300

def app_target
  project.app_target
end

#check_repo_status!Object

Raises:



321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/react_native_util/converter.rb', line 321

def check_repo_status!
  # If the git command is not installed, there's not much we can do.
  return if `which git`.empty?

  `git rev-parse --git-dir > /dev/null 2>&1`
  # Not a git repo
  return unless $?.success?

  `git diff-index --quiet HEAD --`
  return if $?.success?

  raise ConversionError, 'Uncommitted changes in repo. Please commit or stash before continuing.'
end

#convert_to_react_pod!Object

Convert project to use React pod. Expects the app’s package.json in the current directory.

require 'react_native_util/converter'

Dir.chdir '/path/to/rn/project' do
  begin
    ReactNativeUtil::Converter.new(repo_update: true).convert_to_react_pod!
  rescue ReactNativeUtil::BaseException => e
    # All exceptions generated by this gem inherit from
    # ReactNativeUtil::BaseException.
    puts "Error from #convert_to_react_pod!: #{e.message}"
  end
end

Raises:

  • ConversionError on conversion failure

  • ExecutionError on generic command failure

  • Errno::ENOENT if a required command is not present



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/react_native_util/converter.rb', line 58

def convert_to_react_pod!
  startup!

  if project.libraries_group.nil?
    log "Libraries group not found in #{xcodeproj_path}. No conversion necessary."
    return
  end

  if File.exist? podfile_path
    log "Podfile already present at #{File.expand_path podfile_path}.".red.bold
    log "A future release of #{NAME} may support integration with an existing Podfile."
    log 'This release can only convert apps that do not currently use a Podfile.'
    exit 1
  end

  # Not used at the moment
  # load_react_podspec!

  # 1. Detect native dependencies in Libraries group.
  log 'Dependencies:'
  project.dependencies.each { |d| log " #{d}" }

  # Save for after Libraries removed.
  deps_to_add = project.dependencies

  # 2. Run react-native unlink for each one.
  log 'Unlinking dependencies'
  project.dependencies.each do |dep|
    run_command_with_spinner! 'react-native', 'unlink', dep, log: File.join(Dir.tmpdir, "react-native-unlink-#{dep}.log")
  end

  # reload after react-native unlink
  load_xcodeproj!
  load_react_project!

  # 3. Add Start Packager script
  project.add_packager_script_from react_project

  # 4. Generate boilerplate Podfile.
  generate_podfile!

  # 5. Remove Libraries group from Xcode project.
  project.remove_libraries_group
  project.save

  # 6. Run react-native link for each dependency (adds to Podfile).
  log 'Linking dependencies'
  deps_to_add.each do |dep|
    run_command_with_spinner! 'react-native', 'link', dep, log: File.join(Dir.tmpdir, "react-native-link-#{dep}.log")
  end

  # 7. pod install
  # TODO: Can this be customized? Is this the only thing I have to look for
  # in case CocoaPods needs an initial setup? We pull it in as a dependency.
  # It could be they've never set it up before. Assume if this directory
  # exists, pod install is possible (possibly with --repo-update).
  master_podspec_repo_path = File.join ENV['HOME'], '.cocoapods', 'repos', 'master'
  unless Dir.exist?(master_podspec_repo_path)
    # The worst thing that can happen is this is equivalent to pod repo update.
    # But then pod install --repo-update will take very little time.
    log 'Setting up CocoaPods'
    run_command_with_spinner! 'pod', 'setup', log: File.join(Dir.tmpdir, 'pod-setup.log')
  end

  log "Generating Pods project and ios/#{app_name}.xcworkspace".bold.cyan
  log 'Once pod install is complete, your project will be part of this workspace.'.bold.cyan
  log 'From now on, you should build the workspace with Xcode instead of the project.'.bold.cyan
  log 'Always add the workspace and Podfile.lock to SCM.'.bold.cyan
  log 'It is common practice also to add the Pods directory.'.bold.cyan
  log 'The workspace will be automatically opened when pod install completes.'.bold.cyan
  command = %w[pod install]
  command << '--repo-update' if options[:repo_update]
  run_command_with_spinner!(*command, chdir: 'ios', log: File.join(Dir.tmpdir, 'pod-install.log'))

  log 'Conversion complete ✅'

  # 8. Open workspace/build
  execute 'open', File.join('ios', "#{app_name}.xcworkspace")

  # 9. TODO: SCM/git (add, commit - optional)
  # See https://github.com/jdee/react_native_util/issues/18
end

#generate_podfile!Object

Generate a Podfile from a template.



313
314
315
316
317
318
319
# File 'lib/react_native_util/converter.rb', line 313

def generate_podfile!
  log "Generating #{podfile_path}"
  podfile_contents = ERB.new(File.read(PODFILE_TEMPLATE_PATH)).result binding
  File.open podfile_path, 'w' do |file|
    file.write podfile_contents
  end
end

#install_npm_deps_if_needed!Object



217
218
219
220
221
222
223
224
225
# File 'lib/react_native_util/converter.rb', line 217

def install_npm_deps_if_needed!
  raise ConversionError, 'package.json not found. Please run from the project root.' unless File.readable?('package.json')

  execute 'yarn', 'check', '--integrity', log: nil, output: :close
  execute 'yarn', 'check', '--verify-tree', log: nil, output: :close
rescue ExecutionError
  # install deps if either check fails
  run_command_with_spinner! 'yarn', 'install', log: File.join(Dir.tmpdir, 'yarn.log')
end

#load_package_json!Object

Read the contents of ./package.json into @package_json

Raises:

  • ConversionError on failure



209
210
211
212
213
214
215
# File 'lib/react_native_util/converter.rb', line 209

def load_package_json!
  @package_json = File.open('package.json') { |f| JSON.parse f.read }
rescue Errno::ENOENT
  raise ConversionError, 'Failed to load package.json. File not found. Please run from the project root.'
rescue JSON::ParserError => e
  raise ConversionError, "Failed to parse package.json: #{e.message}"
end

#load_react_podspec!Object



283
284
285
286
287
288
289
290
291
292
# File 'lib/react_native_util/converter.rb', line 283

def load_react_podspec!
  podspec_dir = 'node_modules/react-native'
  podspec_contents = File.read "#{podspec_dir}/React.podspec"
  podspec_contents.gsub!(/__dir__/, podspec_dir.inspect)

  require 'cocoapods-core'
  # rubocop: disable Security/Eval
  @react_podspec = eval(podspec_contents)
  # rubocop: enable Security/Eval
end

#load_react_project!Object

Load the contents of the React.xcodeproj project from node_modules.

Raises:

  • Xcodeproj::PlainInformative in case of most failures



337
338
339
# File 'lib/react_native_util/converter.rb', line 337

def load_react_project!
  @react_project = Project.open 'node_modules/react-native/React/React.xcodeproj'
end

#load_xcodeproj!Object

Load the project at @xcodeproj_path into @xcodeproj

Raises:

  • ConversionError on failure



273
274
275
276
277
278
279
280
281
# File 'lib/react_native_util/converter.rb', line 273

def load_xcodeproj!
  @project = nil # in case of exception on reopen
  @project = Project.open xcodeproj_path
  @project.app_name = app_name
rescue Errno::ENOENT
  raise ConversionError, "Failed to open #{xcodeproj_path}. File not found."
rescue Xcodeproj::PlainInformative => e
  raise ConversionError, "Failed to load #{xcodeproj_path}: #{e.message}"
end

#log_command_path(command, package = command, include_version: true) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/react_native_util/converter.rb', line 256

def log_command_path(command, package = command, include_version: true)
  path = `which #{command}`
  if path.empty?
    log " #{package}: #{'not found'.red.bold}"
    return
  end

  if include_version
    version = `#{command} --version`.chomp
    log " #{package} #{version}: #{path}"
  else
    log " #{package}: #{path}"
  end
end

#podfile_pathObject



308
309
310
# File 'lib/react_native_util/converter.rb', line 308

def podfile_path
  'ios/Podfile'
end

#report_configuration!Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/react_native_util/converter.rb', line 227

def report_configuration!
  log "#{NAME} react_pod v#{VERSION}".bold

  install_npm_deps_if_needed!

  log ' Installed from Homebrew' if ENV['REACT_NATIVE_UTIL_INSTALLED_FROM_HOMEBREW']

  log " #{`uname -msr`}"

  log " Ruby #{RUBY_VERSION}: #{RbConfig.ruby}"
  log " RubyGems #{Gem::VERSION}: #{`which gem`}"
  log " Bundler #{Bundler::VERSION}: #{`which bundle`}" if defined?(Bundler)

  log_command_path 'react-native', 'react-native-cli', include_version: false
  unless `which react-native`.empty?
    react_native_info = `react-native --version`
    react_native_info.split("\n").each { |l| log "  #{l}" }
  end

  log_command_path 'yarn'
  log_command_path 'pod', 'cocoapods'

  log " cocoapods-core: #{Pod::CORE_VERSION}"
rescue Errno::ENOENT
  # On Windows, e.g., which and uname may not work.
  log 'Conversion failed: macOS required.'.red.bold
  exit(-1)
end

#startup!Object

Raises:



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/react_native_util/converter.rb', line 185

def startup!
  validate_commands! REQUIRED_COMMANDS

  # Make sure no uncommitted changes
  check_repo_status!

  report_configuration!

  raise ConversionError, "macOS required." unless mac?

  load_package_json!
  log 'package.json:'
  log " app name: #{app_name.inspect}"

  # Detect project. TODO: Add an option to override.
  @xcodeproj_path = File.expand_path "ios/#{app_name}.xcodeproj"
  load_xcodeproj!
  log "Found Xcode project at #{xcodeproj_path}"

  project.validate_app_target!
end

#test_targetObject



304
305
306
# File 'lib/react_native_util/converter.rb', line 304

def test_target
  project.test_target
end

#update_project!Object

Raises:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/react_native_util/converter.rb', line 141

def update_project!
  startup!

  unless project.libraries_group.nil?
    raise ConversionError, "Libraries group present in #{xcodeproj_path}. Conversion necessary. Run rn react_pod without -u."
  end

  unless File.exist? podfile_path
    raise ConversionError, "#{podfile_path} not found. Conversion necessary. Run rn react_pod without -u."
  end

  log "Updating project at #{xcodeproj_path}"

  # Check/update the contents of the packager script in React.xcodeproj
  load_react_project!

  current_script_phase = project.packager_phase

  # Not an error. User may have removed it.
  log "Packager build phase not found in #{xcodeproj_path}. Not updating.".yellow and return if current_script_phase.nil?

  new_script_phase = react_project.packager_phase
  # Totally unexpected. Exception. TODO: This is not treated as an error on conversion. Probably should be.
  raise ConversionError, "Packager build phase not found in #{react_project.path}." if new_script_phase.nil?

  new_script = new_script_phase.shell_script.gsub %r{../scripts}, '../node_modules/react-native/scripts'

  if new_script == current_script_phase.shell_script && new_script_phase.name == current_script_phase.name
    log "#{current_script_phase.name} build phase up to date. ✅"
    return
  end

  log 'Updating packager phase.'
  log " Current name: #{current_script_phase.name}"
  log " New name    : #{new_script_phase.name}"

  current_script_phase.name = new_script_phase.name
  current_script_phase.shell_script = new_script

  project.save

  log "Updated #{xcodeproj_path}"
end