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

.artifact_source_signature(roles: nil) ⇒ Object

Returns a cheap freshness signature for every local source that affects the requested artifact roles. URL-backed sources return nil because they have no trustworthy filesystem metadata and must remain volatile.



77
78
79
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 77

def artifact_source_signature(roles: nil)
  RendererArtifactSupport.source_signature(self, roles:)
end

.asset_basename(asset) ⇒ Object



205
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 205

def asset_basename(asset) = RendererArtifactSupport.asset_basename(asset)

.asset_label(asset_path) ⇒ Object



194
195
196
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 194

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

.build_current_artifacts(action_description:, url_loader: method(:load_url_companion), roles: nil) ⇒ Object

Resolves the complete runtime-visible renderer artifact set once. Every producer must use these value objects so the cache directory ID and the bytes staged or uploaded under it cannot be assembled from different companion lists.



70
71
72
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 70

def build_current_artifacts(action_description:, url_loader: method(:load_url_companion), roles: nil)
  RendererArtifactSupport.build(self, action_description:, url_loader:, roles:)
end

.bundle_sources(_pool, action_description) ⇒ Object

Legacy compatibility wrapper returning [path, ID] pairs. New internal callers consume RendererArtifact values directly so the captured bytes stay bound to the ID. Reuse each already-built artifact ID here instead of asking the pool to perform a second, potentially divergent build.



306
307
308
309
310
311
312
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 306

def bundle_sources(_pool, action_description)
  artifacts = ReactOnRailsPro::Utils.renderer_artifacts(action_description:)
  artifacts.map do |artifact|
    validate_bundle_hash!(artifact.id, artifact.bundle)
    [artifact.bundle, artifact.id]
  end
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.



62
63
64
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 62

def collect_assets
  collect_assets_with_required_paths.first
end

.collect_assets_with_required_pathsObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 34

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



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

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.



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

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)


201
202
203
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 201

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

.load_url_companion(url) ⇒ Object



85
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 85

def load_url_companion(url) = RendererArtifactSupport.load_url(url)

.loadable_stats_asset_pathObject



129
130
131
132
133
134
135
136
137
138
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 129

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


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

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


294
295
296
297
298
299
300
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 294

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


285
286
287
288
289
290
291
292
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 285

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



87
88
89
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 87

def required_rsc_asset_basenames
  required_rsc_asset_paths_for_current_config.map { |path| asset_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.

Keep URL-backed manifests as their URL strings. Development/test may materialize them, while production-like builds must recognize them as required and fail instead of silently publishing an incomplete artifact.



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

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

  Set.new(
    manifests.map do |path|
      http_url?(path) ? path.to_s : File.expand_path(path.to_s, Rails.root)
    end
  )
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.



95
96
97
98
99
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 95

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

.required_source?(source, required_paths) ⇒ Boolean

Returns:

  • (Boolean)


225
226
227
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 225

def required_source?(source, required_paths)
  RendererArtifactSupport.required_source?(source, required_paths)
end

.rsc_manifest_pathsObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 101

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



277
278
279
280
281
282
283
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 277

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

.stageable_companion_mapping(assets, required_paths, action_description, url_loader: method(:load_url_companion)) ⇒ Object



81
82
83
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 81

def stageable_companion_mapping(assets, required_paths, action_description, url_loader: method(:load_url_companion))
  RendererArtifactSupport.stageable_mapping(self, assets, required_paths, action_description, url_loader:)
end

.validate_bundle_exists!(path, action_description) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 229

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

  raise ReactOnRailsPro::Error,
        "Bundle exists but is not a file at #{path}. " \
        "Please configure a bundle file before #{action_description} the renderer cache."
rescue Errno::ENOENT, Errno::ENOTDIR
  raise ReactOnRailsPro::MissingRendererBundleError,
        "Bundle not found 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.



250
251
252
253
254
255
256
257
258
259
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 250

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.



120
121
122
123
124
125
126
127
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 120

def warn_on_duplicate_basenames(assets)
  basenames = assets.map { |asset| asset_basename(asset) }
  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

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



190
191
192
# File 'lib/react_on_rails_pro/renderer_cache_helpers.rb', line 190

def write_content_atomically(content, dest, log_prefix:, source_label: nil)
  RendererArtifactSupport.write_content_atomically(content, dest, log_prefix:, source_label:)
end