Module: ReactOnRailsPro::RendererArtifactSupport

Defined in:
lib/react_on_rails_pro/renderer_artifact_support.rb

Overview

Assembly and immutable-byte helpers used by RendererCacheHelpers. Keeping artifact construction separate makes the cache/staging API easier to audit.

Class Method Summary collapse

Class Method Details

.asset_basename(asset) ⇒ Object



214
215
216
217
218
219
220
221
222
223
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 214

def asset_basename(asset)
  return File.basename(asset.to_s) unless asset.to_s.match?(%r{\Ahttps?://})

  basename = File.basename(URI.parse(asset.to_s).path)
  return basename unless basename.empty?

  raise ReactOnRailsPro::Error, "URL-backed renderer companion has no destination basename: #{asset}"
rescue URI::InvalidURIError => e
  raise ReactOnRailsPro::Error, "Invalid URL-backed renderer companion #{asset.inspect}: #{e.message}"
end

.build(cache_helpers, action_description:, url_loader:, roles: nil) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 31

def build(cache_helpers, action_description:, url_loader:, roles: nil)
  selected_roles = normalize_roles(roles)
  bundles = selected_bundle_paths(selected_roles)
  validate_selected_bundle_paths!(cache_helpers, bundles, action_description)
  assets, required_paths = cache_helpers.collect_assets_with_required_paths
  # Server-only callers still identify any RSC manifests that already exist,
  # but a not-yet-built RSC output must not prevent plain SSR cache keys.
  # Selecting the RSC role keeps both manifests fail-loud and required.
  required_paths = Set.new unless selected_roles.include?(:rsc)
  companions = stageable_mapping(cache_helpers, assets, required_paths, action_description, url_loader:)

  artifacts = []
  server_artifact = if selected_roles.include?(:server)
                      build_bundle_artifact(
                        cache_helpers,
                        role: :server,
                        bundle: bundles.fetch(:server),
                        companions:,
                        action_description:,
                        url_loader:
                      ).tap { |artifact| artifacts << artifact }
                    end
  if selected_roles.include?(:rsc)
    artifacts << build_bundle_artifact(
      cache_helpers,
      role: :rsc,
      bundle: bundles.fetch(:rsc),
      companions:,
      companion_bodies: server_artifact&.companion_bodies,
      action_description:,
      url_loader:
    )
  end
  artifacts.freeze
end

.load_url(url) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 187

def load_url(url)
  response = RendererHttpClient.get(
    url,
    connect_timeout: ReactOnRailsPro.configuration.renderer_http_pool_timeout,
    read_timeout: ReactOnRailsPro.configuration.ssr_timeout
  )
  raise RendererHttpClient::HTTPError, response if response.error?

  response.body
rescue RendererHttpClient::Error => e
  detail = e.is_a?(RendererHttpClient::HTTPError) ? e.response.body : e.message
  raise ReactOnRails::ServerBundleLoadError, "Failed to fetch dev-server asset from #{url}: #{detail}"
end

.required_source?(source, required_paths, rails_root: Rails.root) ⇒ Boolean

Returns:

  • (Boolean)


225
226
227
228
229
230
231
232
233
234
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 225

def required_source?(source, required_paths, rails_root: Rails.root)
  key = if source.to_s.match?(%r{\Ahttps?://})
          source.to_s
        else
          File.expand_path(source.to_s, rails_root)
        end
  required_paths.include?(key)
rescue ArgumentError
  false
end

.source_signature(cache_helpers, roles: nil) ⇒ Object

Metadata-only freshness check for development/test hash lookups. The signature covers every selected bundle plus every configured companion, including missing optional paths so later appearance invalidates it. URL-backed inputs are deliberately volatile because a URL has no local metadata capable of proving that its response body is unchanged.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 93

def source_signature(cache_helpers, roles: nil)
  selected_roles = normalize_roles(roles)
  assets, = cache_helpers.collect_assets_with_required_paths
  entries = selected_roles.map do |role|
    bundle = if role == :server
               ReactOnRails::Utils.server_bundle_js_file_path
             else
               ReactOnRailsPro::Utils.rsc_bundle_js_file_path
             end
    [:bundle, role, bundle]
  end
  entries.concat(assets.map { |asset| [:companion, cache_helpers.asset_basename(asset), asset] })
  return nil if entries.any? { |_, _, source| cache_helpers.http_url?(source) }

  entries.map { |kind, name, source| (kind, name, source) }.freeze
end

.stageable_mapping(cache_helpers, assets, required_paths, action_description, url_loader:) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 174

def stageable_mapping(cache_helpers, assets, required_paths, action_description, url_loader:)
  assets.each_with_object({}) do |asset_path, mapping|
    companion = stageable_companion(
      cache_helpers,
      asset_path,
      required_paths,
      action_description,
      url_loader:
    )
    mapping[companion.fetch(:basename)] = companion.fetch(:source) if companion
  end.freeze
end

.write_content_atomically(content, dest, log_prefix:, source_label: nil) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/react_on_rails_pro/renderer_artifact_support.rb', line 201

def write_content_atomically(content, dest, log_prefix:, source_label: nil)
  FileUtils.mkdir_p(File.dirname(dest))
  tmp_file = "#{dest}.tmp-#{Process.pid}-#{SecureRandom.hex(6)}"
  File.binwrite(tmp_file, content)
  File.rename(tmp_file, dest)
  return unless log_prefix

  operation = source_label ? "#{source_label} -> #{dest}" : dest
  puts "[ReactOnRailsPro] #{log_prefix}: #{operation}"
ensure
  FileUtils.rm_f(tmp_file) if tmp_file
end