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
-
.artifact_source_signature(roles: nil) ⇒ Object
Returns a cheap freshness signature for every local source that affects the requested artifact roles.
- .asset_basename(asset) ⇒ Object
- .asset_label(asset_path) ⇒ Object
-
.build_current_artifacts(action_description:, url_loader: method(:load_url_companion), roles: nil) ⇒ Object
Resolves the complete runtime-visible renderer artifact set once.
-
.bundle_sources(_pool, action_description) ⇒ Object
Legacy compatibility wrapper returning [path, ID] pairs.
-
.collect_assets ⇒ Object
Convenience for callers that only need the asset list and intentionally discard the rsc_required_paths Set returned by collect_assets_with_required_paths.
- .collect_assets_with_required_paths ⇒ Object
- .copy_file_atomically(src, dest, log_prefix:) ⇒ Object
-
.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.
-
.http_url?(path) ⇒ Boolean
Mirrors
Request#http_url?: detects dev-server-served assets returned byReactOnRails::PackerUtils.asset_uri_from_packerso the staging path can skip them instead of treating them as filesystem paths. - .load_url_companion(url) ⇒ Object
- .loadable_stats_asset_path ⇒ Object
- .make_relative_symlink(source, destination, log_prefix:) ⇒ Object
- .realpath_for_symlink_destination(destination_dir) ⇒ Object
- .realpath_for_symlink_source(source) ⇒ Object
- .required_rsc_asset_basenames ⇒ Object
-
.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.
-
.required_rsc_asset_paths_for_current_config ⇒ Object
No-arg companion to
required_rsc_asset_pathsfor callers (rolling-deploy adapter publication, payload validation) that don't already hold the resolved manifest list. - .required_source?(source, required_paths) ⇒ Boolean
- .rsc_manifest_paths ⇒ Object
- .stage_file(src, dest, mode, log_prefix:) ⇒ Object
- .stageable_companion_mapping(assets, required_paths, action_description, url_loader: method(:load_url_companion)) ⇒ Object
- .validate_bundle_exists!(path, action_description) ⇒ Object
-
.validate_bundle_hash!(hash, path) ⇒ Object
Defense-in-depth against future regressions in the hash-computation path:
calc_bundle_hashalways returns a non-empty string today, but a blank value here would causeFile.join(cache_dir, "")to resolve tocache_diritself and stage the bundle as<cache_dir>/.js— a hidden file the renderer never reads. -
.warn_on_duplicate_basenames(assets) ⇒ Object
stage_assetswrites each asset intobundle_dirusing only its basename, so two distinct assets with the same basename (e.g./path/a/manifest.jsonand/path/b/manifest.json) silently overwrite one another. - .write_content_atomically(content, dest, log_prefix:, source_label: nil) ⇒ Object
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_assets ⇒ Object
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_paths ⇒ Object
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 = begin File.(asset_path.to_s, Rails.root) rescue ArgumentError => e warn "[ReactOnRailsPro] Asset not found #{asset_label(asset_path)} (invalid path: #{e.})" next end unless File.file?() if rsc_required_paths.include?() 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 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.
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_path ⇒ Object
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 |
.make_relative_symlink(source, destination, log_prefix:) ⇒ Object
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 |
.realpath_for_symlink_destination(destination_dir) ⇒ Object
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 |
.realpath_for_symlink_source(source) ⇒ Object
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_basenames ⇒ Object
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.(path.to_s, Rails.root) end ) end |
.required_rsc_asset_paths_for_current_config ⇒ Object
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
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_paths ⇒ Object
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 |