require 'action_view'
require 'sprockets'
require 'active_support/core_ext/class/attribute'
require 'sprockets/rails/utils'
module Sprockets
module Rails
module Helper
class AssetNotFound < StandardError; end
class AssetNotPrecompiled < StandardError; end
class AssetNotPrecompiledError < AssetNotPrecompiled
include Sprockets::Rails::Utils
def initialize(source)
msg =
if using_sprockets4?
"Asset `#{ source }` was not declared to be precompiled in production.\n" +
"Declare links to your assets in `app/assets/config/manifest.js`.\n\n" +
" //= link #{ source }\n\n" +
"and restart your server"
else
"Asset was not declared to be precompiled in production.\n" +
"Add `Rails.application.config.assets.precompile += " +
"%w( #{source} )` to `config/initializers/assets.rb` and " +
"restart your server"
end
super(msg)
end
end
include ActionView::Helpers::AssetUrlHelper
include ActionView::Helpers::AssetTagHelper
include Sprockets::Rails::Utils
VIEW_ACCESSORS = [
:assets_environment, :assets_manifest,
:assets_precompile, :precompiled_asset_checker,
:assets_prefix, :digest_assets, :debug_assets,
:resolve_assets_with, :check_precompiled_asset,
:unknown_asset_fallback
]
def self.included(klass)
klass.class_attribute(*VIEW_ACCESSORS)
klass.class_eval do
remove_method :assets_environment
def assets_environment
if instance_variable_defined?(:@assets_environment)
@assets_environment = @assets_environment.cached
elsif env = self.class.assets_environment
@assets_environment = env.cached
else
nil
end
end
end
end
def self.extended(obj)
obj.singleton_class.class_eval do
attr_accessor(*VIEW_ACCESSORS)
remove_method :assets_environment
def assets_environment
if env = @assets_environment
@assets_environment = env.cached
else
nil
end
end
end
end
# Writes over the built in ActionView::Helpers::AssetUrlHelper#compute_asset_path
# to use the asset pipeline.
def compute_asset_path(path, options = {})
debug = options[:debug]
if asset_path = resolve_asset_path(path, debug)
File.join(assets_prefix || "/", legacy_debug_path(asset_path, debug))
else
message = "The asset #{ path.inspect } is not present in the asset pipeline.\n"
raise AssetNotFound, message unless unknown_asset_fallback
if respond_to?(:public_compute_asset_path)
message << "Falling back to an asset that may be in the public folder.\n"
message << "This behavior is deprecated and will be removed.\n"
message << "To bypass the asset pipeline and preserve this behavior,\n"
message << "use the `skip_pipeline: true` option.\n"
call_stack = Kernel.respond_to?(:caller_locations) && ::Rails::VERSION::MAJOR >= 5 ? caller_locations : caller
ActiveSupport::Deprecation.warn(message, call_stack)
end
super
end
end
# Resolve the asset path against the Sprockets manifest or environment.
# Returns nil if it's an asset we don't know about.
def resolve_asset_path(path, allow_non_precompiled = false) #:nodoc:
resolve_asset do |resolver|
resolver.asset_path path, digest_assets, allow_non_precompiled
end
end
# Expand asset path to digested form.
#
# path - String path
# options - Hash options
#
# Returns String path or nil if no asset was found.
def asset_digest_path(path, options = {})
resolve_asset do |resolver|
resolver.digest_path path, options[:debug]
end
end
# Experimental: Get integrity for asset path.
#
# path - String path
# options - Hash options
#
# Returns String integrity attribute or nil if no asset was found.
def asset_integrity(path, options = {})
path = path_with_extname(path, options)
resolve_asset do |resolver|
resolver.integrity path
end
end
# Override javascript tag helper to provide debugging support.
#
# Eventually will be deprecated and replaced by source maps.
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
integrity = compute_integrity?(options)
if options["debug"] != false && request_debug_assets?
sources.map { |source|
if asset = lookup_debug_asset(source, type: :javascript)
if asset.respond_to?(:to_a)
asset.to_a.map do |a|
super(path_to_javascript(a.logical_path, debug: true), options)
end
else
super(path_to_javascript(asset.logical_path, debug: true), options)
end
else
super(source, options)
end
}.flatten.uniq.join("\n").html_safe
else
sources.map { |source|
options = options.merge('integrity' => asset_integrity(source, type: :javascript)) if integrity
super source, options
}.join("\n").html_safe
end
end
# Override stylesheet tag helper to provide debugging support.
#
# Eventually will be deprecated and replaced by source maps.
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
integrity = compute_integrity?(options)
if options["debug"] != false && request_debug_assets?
sources.map { |source|
if asset = lookup_debug_asset(source, type: :stylesheet)
if asset.respond_to?(:to_a)
asset.to_a.map do |a|
super(path_to_stylesheet(a.logical_path, debug: true), options)
end
else
super(path_to_stylesheet(asset.logical_path, debug: true), options)
end
else
super(source, options)
end
}.flatten.uniq.join("\n").html_safe
else
sources.map { |source|
options = options.merge('integrity' => asset_integrity(source, type: :stylesheet)) if integrity
super source, options
}.join("\n").html_safe
end
end
protected
# This is awkward: `integrity` is a boolean option indicating whether
# we want to include or omit the subresource integrity hash, but the
# options hash is also passed through as literal tag attributes.
# That means we have to delete the shortcut boolean option so it
# doesn't bleed into the tag attributes, but also check its value if
# it's boolean-ish.
def compute_integrity?(options)
if secure_subresource_integrity_context?
case options['integrity']
when nil, false, true
options.delete('integrity') == true
end
else
options.delete 'integrity'
false
end
end
# Only serve integrity metadata for HTTPS requests:
# http://www.w3.org/TR/SRI/#non-secure-contexts-remain-non-secure
def secure_subresource_integrity_context?
respond_to?(:request) && self.request && (self.request.local? || self.request.ssl?)
end
# Enable split asset debugging. Eventually will be deprecated
# and replaced by source maps in Sprockets 3.x.
def request_debug_assets?
debug_assets || (defined?(controller) && controller && params[:debug_assets])
rescue # FIXME: what exactly are we rescuing?
false
end
# Internal method to support multifile debugging. Will
# eventually be removed w/ Sprockets 3.x.
def lookup_debug_asset(path, options = {})
path = path_with_extname(path, options)
resolve_asset do |resolver|
resolver.find_debug_asset path
end
end
# compute_asset_extname is in AV::Helpers::AssetUrlHelper
def path_with_extname(path, options)
path = path.to_s
"#{path}#{compute_asset_extname(path, options)}"
end
# Try each asset resolver and return the first non-nil result.
def resolve_asset
asset_resolver_strategies.detect do |resolver|
if result = yield(resolver)
break result
end
end
end
# List of resolvers in `config.assets.resolve_with` order.
def asset_resolver_strategies
@asset_resolver_strategies ||=
Array(resolve_assets_with).map do |name|
HelperAssetResolvers[name].new(self)
end
end
# Append ?body=1 if debug is on and we're on old Sprockets.
def legacy_debug_path(path, debug)
if debug && !using_sprockets4?
"#{path}?body=1"
else
path
end
end
end
# Use a separate module since Helper is mixed in and we needn't pollute
# the class namespace with our internals.
module HelperAssetResolvers #:nodoc:
def self.[](name)
case name
when :manifest
Manifest
when :environment
Environment
else
raise ArgumentError, "Unrecognized asset resolver: #{name.inspect}. Expected :manifest or :environment"
end
end
class Manifest #:nodoc:
def initialize(view)
@manifest = view.assets_manifest
raise ArgumentError, 'config.assets.resolve_with includes :manifest, but app.assets_manifest is nil' unless @manifest
end
def asset_path(path, digest, allow_non_precompiled = false)
if digest
digest_path path, allow_non_precompiled
end
end
def digest_path(path, allow_non_precompiled = false)
@manifest.assets[path]
end
def integrity(path)
if meta = metadata(path)
meta["integrity"]
end
end
def find_debug_asset(path)
nil
end
private
def metadata(path)
if digest_path = digest_path(path)
@manifest.files[digest_path]
end
end
end
class Environment #:nodoc:
def initialize(view)
raise ArgumentError, 'config.assets.resolve_with includes :environment, but app.assets is nil' unless view.assets_environment
@env = view.assets_environment
@precompiled_asset_checker = view.precompiled_asset_checker
@check_precompiled_asset = view.check_precompiled_asset
end
def asset_path(path, digest, allow_non_precompiled = false)
# Digests enabled? Do the work to calculate the full asset path.
if digest
digest_path path, allow_non_precompiled
# Otherwise, ask the Sprockets environment whether the asset exists
# and check whether it's also precompiled for production deploys.
elsif asset = find_asset(path)
raise_unless_precompiled_asset asset.logical_path unless allow_non_precompiled
path
end
end
def digest_path(path, allow_non_precompiled = false)
if asset = find_asset(path)
raise_unless_precompiled_asset asset.logical_path unless allow_non_precompiled
asset.digest_path
end
end
def integrity(path)
find_asset(path).try :integrity
end
def find_debug_asset(path)
if asset = find_asset(path, pipeline: :debug)
raise_unless_precompiled_asset asset.logical_path.sub('.debug', '')
asset
end
end
private
if RUBY_VERSION >= "2.7"
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def find_asset(path, options = {})
@env[path, **options]
end
RUBY
else
def find_asset(path, options = {})
@env[path, options]
end
end
def precompiled?(path)
@precompiled_asset_checker.call path
end
def raise_unless_precompiled_asset(path)
raise Helper::AssetNotPrecompiledError.new(path) if @check_precompiled_asset && !precompiled?(path)
end
end
end
end
end