1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 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 142 143 144 145 146 147 148 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
|
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
|