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
- .asset_label(asset_path) ⇒ Object
-
.bundle_sources(pool, action_description) ⇒ Object
Resolves bundle sources as [path, hash] pairs so callers can iterate without needing to re-call pool methods.
-
.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 by `ReactOnRails::PackerUtils.asset_uri_from_packer` so the staging path can skip them instead of treating them as filesystem paths.
- .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_paths` for callers (rolling-deploy adapter publication, payload validation) that don’t already hold the resolved manifest list.
- .rsc_manifest_paths ⇒ Object
- .stage_file(src, dest, mode, log_prefix:) ⇒ 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_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.
-
.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.
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_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.
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_paths ⇒ Object
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 = 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.
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_path ⇒ Object
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 |
.make_relative_symlink(source, destination, log_prefix:) ⇒ Object
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 |
.realpath_for_symlink_destination(destination_dir) ⇒ Object
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 |
.realpath_for_symlink_source(source) ⇒ Object
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_basenames ⇒ Object
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.(path.to_s, Rails.root) } ) 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.
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_paths ⇒ Object
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 |