Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions lib/reactionview/asset_manifest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

require "json"

module ReActionView
class AssetManifest
def initialize(assets)
@assets = assets
end

def path
config = @assets&.config

return unless config.respond_to?(:manifest_path)

config.manifest_path
end

def mtime
return if path.nil?

File.mtime(path)
rescue SystemCallError
nil
end

def exist?
!mtime.nil?
end

def missing_assets
precompiled = logical_paths

return [] if precompiled.empty?

ReActionView::Railtie::PRECOMPILE_ASSETS - precompiled
end

def stale?
exist? && !missing_assets.empty?
end

def in_use?
@assets.respond_to?(:resolver) && @assets.resolver.respond_to?(:manifest_path)
end

def explanation
<<~MESSAGE
ReActionView's dev tools assets are missing from your precompiled assets.
#{status}

To fix this, delete the precompiled assets:

bin/rails assets:clobber

That is safe in development. Without public/assets/, Propshaft serves each asset straight from
the directory it lives in, including ReActionView's.

Missing: #{missing_assets.join(", ")}
Manifest: #{path}

public/assets/ was most likely left behind by `RAILS_ENV=production bin/rails assets:precompile`,
and a production precompile doesn't include ReActionView's dev tools.
MESSAGE
end

private

def status
if in_use?
"Because public/assets/.manifest.json exists, Propshaft serves every asset from public/assets/\n" \
"and doesn't look anywhere else, which is why the lookup failed."
else
"This server booted before public/assets/.manifest.json existed, so pages still render for now.\n" \
"After the next restart Propshaft will serve assets from public/assets/ only, and rendering\n" \
"will fail then."
end
end

def logical_paths
return [] if path.nil?

JSON.parse(File.read(path)).keys
rescue SystemCallError, JSON::ParserError
[]
end
end
end
94 changes: 94 additions & 0 deletions lib/reactionview/middleware/asset_manifest_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require_relative "../asset_manifest"
require_relative "../stale_asset_manifest_error"

module ReActionView
module Middleware
class AssetManifestCheck
def initialize(app, assets: nil, logger: nil)
@app = app
@assets = assets
@logger = logger
@mutex = Mutex.new
@checked_mtime = nil
end

def call(env)
warn_about_upcoming_failure

@app.call(env)
rescue StandardError => e
raise unless dev_tools_asset_missing?(e)

manifest = self.manifest

raise unless manifest.stale?

raise StaleAssetManifestError.for(manifest)
end

private

def assets
@assets || (defined?(Rails) && Rails.application&.assets)
end

def logger
@logger || (defined?(Rails) && Rails.logger)
end

def manifest
AssetManifest.new(assets)
end

def warn_about_upcoming_failure
manifest = self.manifest
mtime = manifest.mtime

return if mtime.nil?

@mutex.synchronize do
return if @checked_mtime == mtime

@checked_mtime = mtime

return if manifest.in_use? || !manifest.stale?

log("[ReActionView] #{manifest.explanation}")
end
end

def dev_tools_asset_missing?(error)
error_class = missing_asset_error_class

return false if error_class.nil?

while error
if error.is_a?(error_class) &&
ReActionView::Railtie::PRECOMPILE_ASSETS.any? { |asset| error.message.include?(asset) }
return true
end

error = error.cause
end

false
end

def missing_asset_error_class
return unless defined?(::Propshaft::MissingAssetError)

::Propshaft::MissingAssetError
end

def log(message)
if logger
logger.warn(message)
else
warn(message)
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/reactionview/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class Railtie < Rails::Railtie
end
end

initializer "reactionview.asset_manifest_check" do |app|
next unless ReActionView.config.development?

require_relative "middleware/asset_manifest_check"

app.middleware.use ReActionView::Middleware::AssetManifestCheck
end

initializer "reactionview.register_herb_handler" do
ActiveSupport.on_load(:action_view) do
ActionView::Template.register_template_handler :herb, ReActionView::Template::Handlers::Herb
Expand Down
9 changes: 9 additions & 0 deletions lib/reactionview/stale_asset_manifest_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module ReActionView
class StaleAssetManifestError < StandardError
def self.for(manifest)
new(manifest.explanation)
end
end
end
Loading
Loading