Module: ReactOnRailsPro::RendererCacheHelpers

Defined in:
lib/react_on_rails_pro/renderer_cache_helpers.rb

Overview

Shared helpers for staging the Node Renderer bundle cache. Used by both PreSeedRendererCache (copies files for Docker images) and PrepareNodeRenderBundles (symlinks for same-filesystem workflows).

Constant Summary collapse

LOADABLE_STATS_ASSET_NAME =
"loadable-stats.json"

Class Method Summary collapse

Class Method Details

.asset_label(asset_path) ⇒ Object



154
155
156
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 154

def asset_label(asset_path)
  asset_path.to_s.empty? ? "<blank>" : asset_path
end

.bundle_sources(pool, action_description) ⇒ Object

Resolves bundle sources as [path, hash] pairs so callers can iterate without needing to re-call pool methods. ‘pool` must respond to `server_bundle_hash` and (when RSC is enabled) `rsc_bundle_hash`.

Validates each bundle path exists before computing its hash, because ‘pool.server_bundle_hash` eventually calls `Digest::MD5.file` / `File.mtime` on the bundle path, which raises raw `Errno::ENOENT` if the file is missing — bypassing the friendly `ReactOnRailsPro::Error` message.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 259

def bundle_sources(pool, action_description)
  server_bundle_path = ReactOnRails::Utils.server_bundle_js_file_path
  validate_bundle_exists!(server_bundle_path, action_description)
  server_hash = pool.server_bundle_hash
  validate_bundle_hash!(server_hash, server_bundle_path)
  sources = [[server_bundle_path, server_hash]]

  return sources unless ReactOnRailsPro.configuration.enable_rsc_support

  rsc_bundle_path = ReactOnRailsPro::Utils.rsc_bundle_js_file_path
  validate_bundle_exists!(rsc_bundle_path, action_description)
  rsc_hash = pool.rsc_bundle_hash
  validate_bundle_hash!(rsc_hash, rsc_bundle_path)
  sources << [rsc_bundle_path, rsc_hash]
  sources
end

.collect_assetsObject

Convenience for callers that only need the asset list and intentionally discard the rsc_required_paths Set returned by collect_assets_with_required_paths. If you need to enforce required-RSC availability (raising loudly when a required manifest is missing), use collect_assets_with_required_paths and pass both into each_stageable_asset — ‘nil`-or-empty here would silently skip the required-paths check.



47
48
49
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 47

def collect_assets
  collect_assets_with_required_paths.first
end

.collect_assets_with_required_pathsObject



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 19

def collect_assets_with_required_paths
  config = ReactOnRailsPro.configuration
  # assets_to_copy may include nil entries (user-configured, optional);
  # those are silently dropped by `.compact`. RSC manifests, by contrast,
  # are required, so resolve them separately and fail loudly if either
  # resolves to nil rather than letting `.compact` swallow the gap.
  assets = Array(config.assets_to_copy).compact
  loadable_stats_path = loadable_stats_asset_path
  assets << loadable_stats_path if loadable_stats_path

  if config.enable_rsc_support
    rsc_manifests = rsc_manifest_paths
    assets.concat(rsc_manifests)
  else
    rsc_manifests = []
  end

  unique = assets.uniq(&:to_s)
  warn_on_duplicate_basenames(unique)
  [unique, required_rsc_asset_paths(rsc_manifests)]
end

.copy_file_atomically(src, dest, log_prefix:) ⇒ Object



143
144
145
146
147
148
149
150
151
152
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 143

def copy_file_atomically(src, dest, log_prefix:)
  FileUtils.mkdir_p(File.dirname(dest))
  tmp_file = "#{dest}.tmp-#{Process.pid}-#{SecureRandom.hex(6)}"
  FileUtils.cp(src, tmp_file)
  File.rename(tmp_file, dest)
  puts "[ReactOnRailsPro] #{log_prefix}: #{src} -> #{dest}"
ensure
  # Clean up the temp file on failure; rm_f is harmless after a successful rename.
  FileUtils.rm_f(tmp_file) if tmp_file
end

.each_stageable_asset(assets, rsc_required_paths, action_description) ⇒ Object

Required assets are matched by expanded path rather than basename so a same-named unrelated entry in assets_to_copy cannot trigger a false- positive “required” error. Expand against Rails.root to match how required_rsc_asset_paths builds its Set.

URL-backed assets (returned by ‘asset_uri_from_packer` while the dev server is running) cannot be staged into the local cache; skip them with a warning so the renderer falls back to fetching them at request time rather than aborting the entire pre-seed.



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
140
141
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 113

def each_stageable_asset(assets, rsc_required_paths, action_description)
  assets.each do |asset_path|
    if http_url?(asset_path)
      warn "[ReactOnRailsPro] Skipping URL-backed asset #{asset_path} while " \
           "#{action_description} the renderer cache; the dev server is serving " \
           "this asset, so the renderer will fetch it on first request."
      next
    end

    expanded =
      begin
        File.expand_path(asset_path.to_s, Rails.root)
      rescue ArgumentError => e
        warn "[ReactOnRailsPro] Asset not found #{asset_label(asset_path)} (invalid path: #{e.message})"
        next
      end

    unless File.file?(expanded)
      if rsc_required_paths.include?(expanded)
        raise ReactOnRailsPro::Error, "Required RSC asset not found or not a file: #{asset_path}. " \
                                      "Build your bundles before #{action_description} the renderer cache."
      end
      warn "[ReactOnRailsPro] Asset not found #{asset_label(asset_path)} (missing or not a file)"
      next
    end

    yield expanded
  end
end

.http_url?(path) ⇒ Boolean

Mirrors ‘Request#http_url?`: detects dev-server-served assets returned by `ReactOnRails::PackerUtils.asset_uri_from_packer` so the staging path can skip them instead of treating them as filesystem paths.

Returns:

  • (Boolean)


161
162
163
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 161

def http_url?(path)
  path.to_s.match?(%r{\Ahttps?://})
end

.loadable_stats_asset_pathObject



93
94
95
96
97
98
99
100
101
102
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 93

def loadable_stats_asset_path
  path = ReactOnRails::PackerUtils.asset_uri_from_packer(LOADABLE_STATS_ASSET_NAME)
  File.exist?(path.to_s) ? path : nil
rescue KeyError, TypeError, Errno::ENOENT
  # Narrow to errors PackerUtils.asset_uri_from_packer can plausibly raise
  # (missing manifest key, nil path, manifest file absent). Unexpected bugs
  # like NoMethodError or NameError should surface so operators can see them
  # rather than being silently swallowed.
  nil
end


210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 210

def make_relative_symlink(source, destination, log_prefix:)
  destination_dir = Pathname.new(destination).dirname
  FileUtils.mkdir_p(destination_dir)

  source_path = realpath_for_symlink_source(source)
  destination_dir_real = realpath_for_symlink_destination(destination_dir)
  relative_source_path = source_path.relative_path_from(destination_dir_real)
  tmp_link = "#{destination}.tmp-#{Process.pid}-#{SecureRandom.hex(6)}"

  File.symlink(relative_source_path.to_s, tmp_link)
  File.rename(tmp_link, destination)
  puts "[ReactOnRailsPro] #{log_prefix}: #{relative_source_path} -> #{destination}"
ensure
  FileUtils.rm_f(tmp_link) if tmp_link
end


243
244
245
246
247
248
249
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 243

def realpath_for_symlink_destination(destination_dir)
  destination_dir.realpath
rescue Errno::ENOENT
  raise ReactOnRailsPro::Error,
        "Cannot resolve real path for symlink destination dir #{destination_dir}" \
        "it may have been removed after mkdir_p (race with an external cleanup)."
end


234
235
236
237
238
239
240
241
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 234

def realpath_for_symlink_source(source)
  Pathname.new(source).realpath
rescue Errno::ENOENT
  raise ReactOnRailsPro::Error,
        "Cannot resolve real path for symlink source #{source}" \
        "it does not exist or is a dangling symlink. " \
        "Rebuild your bundles before staging the renderer cache."
end

.required_rsc_asset_basenamesObject



51
52
53
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 51

def required_rsc_asset_basenames
  required_rsc_asset_paths_for_current_config.map { |path| File.basename(path) }
end

.required_rsc_asset_paths(manifests) ⇒ Object

Must expand against Rails.root so that callers who expand per-asset paths against the same base produce Set-comparable strings. Without an explicit base, File.expand_path uses Dir.pwd, which differs in Docker RUN steps and would make the Set lookup miss.

URL-backed manifests (dev server) cannot be staged; exclude them so ‘each_stageable_asset` does not see them as “required” and raise.



172
173
174
175
176
177
178
179
180
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 172

def required_rsc_asset_paths(manifests)
  return Set.new unless ReactOnRailsPro.configuration.enable_rsc_support

  Set.new(
    manifests
      .reject { |path| http_url?(path) }
      .map { |path| File.expand_path(path.to_s, Rails.root) }
  )
end

.required_rsc_asset_paths_for_current_configObject

No-arg companion to ‘required_rsc_asset_paths` for callers (rolling-deploy adapter publication, payload validation) that don’t already hold the resolved manifest list. Centralising the rsc_manifest_paths lookup avoids call-site drift if the manifest sources change.



59
60
61
62
63
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 59

def required_rsc_asset_paths_for_current_config
  return Set.new unless ReactOnRailsPro.configuration.enable_rsc_support

  required_rsc_asset_paths(rsc_manifest_paths)
end

.rsc_manifest_pathsObject



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 65

def rsc_manifest_paths
  manifests = {
    react_client_manifest_file_path: ReactOnRailsPro::Utils.react_client_manifest_file_path,
    react_server_client_manifest_file_path: ReactOnRailsPro::Utils.react_server_client_manifest_file_path
  }
  nil_manifest_names = manifests.select { |_name, path| path.nil? }.keys
  unless nil_manifest_names.empty?
    raise ReactOnRailsPro::Error,
          "RSC manifest path resolved to nil for #{nil_manifest_names.join(', ')}. " \
          "Check react_client_manifest_file and react_server_client_manifest_file configuration."
  end

  manifests.values
end

.stage_file(src, dest, mode, log_prefix:) ⇒ Object



226
227
228
229
230
231
232
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 226

def stage_file(src, dest, mode, log_prefix:)
  if mode == :copy
    copy_file_atomically(src, dest, log_prefix: log_prefix)
  else
    make_relative_symlink(src, dest, log_prefix: log_prefix)
  end
end

.validate_bundle_exists!(path, action_description) ⇒ Object



182
183
184
185
186
187
188
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 182

def validate_bundle_exists!(path, action_description)
  return if File.file?(path)

  raise ReactOnRailsPro::Error,
        "Bundle not found or not a file at #{path}. " \
        "Please build your bundles before #{action_description} the renderer cache."
end

.validate_bundle_hash!(hash, path) ⇒ Object

Defense-in-depth against future regressions in the hash-computation path: ‘calc_bundle_hash` always returns a non-empty string today, but a blank value here would cause `File.join(cache_dir, “”)` to resolve to `cache_dir` itself and stage the bundle as `<cache_dir>/.js` — a hidden file the renderer never reads. Fail loudly instead of silently mis-staging.

We also reject non-String, non-nil types (e.g. Pathname, Symbol) so a future pool that returns one fails loudly rather than silently producing surprising ‘File.join` results downstream.



199
200
201
202
203
204
205
206
207
208
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 199

def validate_bundle_hash!(hash, path)
  unless hash.nil? || hash.is_a?(String)
    raise ReactOnRailsPro::Error,
          "Bundle hash for #{path} must be a String or nil, got #{hash.class}."
  end
  return unless hash.to_s.strip.empty?

  raise ReactOnRailsPro::Error,
        "Bundle hash for #{path} is nil or blank; cannot stage renderer cache."
end

.warn_on_duplicate_basenames(assets) ⇒ Object

‘stage_assets` writes each asset into `bundle_dir` using only its basename, so two distinct assets with the same basename (e.g. `/path/a/manifest.json` and `/path/b/manifest.json`) silently overwrite one another. Uniq-by-path cannot detect this; warn so the user notices the misconfiguration.



84
85
86
87
88
89
90
91
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 84

def warn_on_duplicate_basenames(assets)
  basenames = assets.reject { |a| http_url?(a) }.map { |a| File.basename(a.to_s) }
  duplicates = basenames.tally.select { |_, count| count > 1 }.keys
  return if duplicates.empty?

  warn "[ReactOnRailsPro] Duplicate asset basenames in assets_to_copy / RSC manifests: " \
       "#{duplicates.join(', ')}. Only the last entry per basename will be staged."
end