Skip to content

Commit a87582b

Browse files
committed
Match Dev Asset Experience to Production
Rails is opinionated about how assets should be served in production. In Production assets should be precompiled and then served directly from the public directory. If an asset is not precompiled, than a 404 will be served instead which can render a site useless if an asset is missing. In development if an asset is __NOT__ flagged to be precompiled (`config.assets.precompile`), and you reference a file: ```ruby <%= asset_path('search.js') %> ``` Then rails will render the file anyway giving the illusion that the your development code is correct although it will fail in production. This PR improves the production experience by raising errors in development. When an asset is referenced via `asset_path` or `asset_url` an error will be raised in non production environments when the asset is not flagged to be precompiled: ```ruby Asset filtered out and will not be served: add `config.assets.precompile += %w( foo.js )` to `config/application.rb` and restart your server ``` This change only affects the user facing methods, the auto concatenation feature for javascript and CSS will still function correctly in debug mode. Assets behaving differently in dev and prod is the number one source of invalid support tickets for Heroku. Since the file displays correctly on their machine locally in development, the production problem is incorrectly attributed to a system problem. Once the source of the problem is identified identified it can be difficult to find the exact file with the problem on systems with many assets. This PR will reduce the chances of deploying broken assets to production by increasing visibility into correct behavior in development. This PR introduces a new config flag: ``` # Development config.assets.raise_runtime_errors = true # Test config.assets.raise_runtime_errors = false # Production config.assets.raise_runtime_errors = false ```
1 parent 25eee98 commit a87582b

File tree

5 files changed

+116
-7
lines changed

5 files changed

+116
-7
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### Master
2+
3+
* Assets not in the precompile list can be checked for errors by setting
4+
`config.assets.raise_runtime_errors = true` in any environment
5+
6+
*Richard Schneeman*
7+
8+
19
### 2.0.1
210

311
* Allow keep value to be specified for `assets:clean` run with args

lib/sprockets/rails/helper.rb

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Helper
88
# support for Ruby 1.9.3 && Rails 3.0.x
99
@_config = ActiveSupport::InheritableOptions.new({}) unless defined?(ActiveSupport::Configurable::Configuration)
1010
include ActiveSupport::Configurable
11-
config_accessor :raise_runtime_errors
11+
config_accessor :precompile, :assets, :raise_runtime_errors
1212

1313
class DependencyError < StandardError
1414
def initialize(path, dep)
@@ -18,6 +18,15 @@ def initialize(path, dep)
1818
end
1919
end
2020

21+
class AssetFilteredError < StandardError
22+
def initialize(source)
23+
msg = "Asset filtered out and will not be served: " <<
24+
"add `config.assets.precompile += %w( #{source} )` " <<
25+
"to `config/application.rb` and restart your server"
26+
super(msg)
27+
end
28+
end
29+
2130
if defined? ActionView::Helpers::AssetUrlHelper
2231
include ActionView::Helpers::AssetUrlHelper
2332
include ActionView::Helpers::AssetTagHelper
@@ -62,6 +71,21 @@ def compute_asset_path(path, options = {})
6271
end
6372
end
6473

74+
# Computes the full URL to a asset in the public directory. This
75+
# method checks for errors before returning path.
76+
def asset_path(source, options = {})
77+
check_errors_for(source)
78+
path_to_asset(source, options)
79+
end
80+
alias :path_to_asset_with_errors :asset_path
81+
82+
# Computes the full URL to a asset in the public directory. This
83+
# will use +asset_path+ internally, so most of their behaviors
84+
# will be the same.
85+
def asset_url(source, options = {})
86+
path_to_asset_with_errors(source, options.merge(:protocol => :request))
87+
end
88+
6589
# Get digest for asset path.
6690
#
6791
# path - String path
@@ -104,6 +128,7 @@ def javascript_include_tag(*sources)
104128

105129
if options["debug"] != false && request_debug_assets?
106130
sources.map { |source|
131+
check_errors_for(source)
107132
if asset = lookup_asset_for_path(source, :type => :javascript)
108133
asset.to_a.map do |a|
109134
super(path_to_javascript(a.logical_path, :debug => true), options)
@@ -123,9 +148,9 @@ def javascript_include_tag(*sources)
123148
# Eventually will be deprecated and replaced by source maps.
124149
def stylesheet_link_tag(*sources)
125150
options = sources.extract_options!.stringify_keys
126-
127151
if options["debug"] != false && request_debug_assets?
128152
sources.map { |source|
153+
check_errors_for(source)
129154
if asset = lookup_asset_for_path(source, :type => :stylesheet)
130155
asset.to_a.map do |a|
131156
super(path_to_stylesheet(a.logical_path, :debug => true), options)
@@ -141,14 +166,33 @@ def stylesheet_link_tag(*sources)
141166
end
142167

143168
protected
144-
145169
# Checks if the asset is included in the dependencies list.
146170
def check_dependencies!(dep)
147171
if raise_runtime_errors && !_dependency_assets.detect { |asset| asset.include?(dep) }
148172
raise DependencyError.new(self.pathname, dep)
149173
end
150174
end
151175

176+
# Raise errors when source does not exist or is not in the precompiled list
177+
def check_errors_for(source)
178+
source = source.to_s
179+
return source if !self.raise_runtime_errors || source.blank? || source =~ URI_REGEXP
180+
asset = lookup_asset_for_path(source)
181+
182+
if asset && asset_needs_precompile?(source, asset.pathname.to_s)
183+
raise AssetFilteredError.new(source)
184+
end
185+
end
186+
187+
# Returns true when an asset will not be available after precompile is run
188+
def asset_needs_precompile?(source, filename)
189+
if assets_environment && assets_environment.send(:matches_filter, precompile || [], source, filename)
190+
false
191+
else
192+
true
193+
end
194+
end
195+
152196
# Enable split asset debugging. Eventually will be deprecated
153197
# and replaced by source maps in Sprockets 3.x.
154198
def request_debug_assets?

lib/sprockets/railtie.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def configure(&block)
113113
app.assets = app.assets.index
114114
end
115115

116+
117+
Sprockets::Rails::Helper.precompile ||= app.config.assets.precompile
118+
Sprockets::Rails::Helper.assets ||= app.assets
116119
Sprockets::Rails::Helper.raise_runtime_errors = app.config.assets.raise_runtime_errors
117120

118121
if config.assets.compile

test/fixtures/error/dependency.js.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<%= asset_path("bar.js") %>
1+
<%= asset_path("bar.js") %>

test/test_helper.rb

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,56 @@ def test_stylesheet_path
372372
assert_equal "/assets/foo-#{@foo_css_digest}.css", @view.stylesheet_path("foo")
373373
end
374374

375+
def test_public_folder_fallback_works_correctly
376+
@view.raise_runtime_errors = true
377+
@view.debug_assets = true
378+
379+
@view.asset_path("asset-does-not-exist-foo.js")
380+
@view.asset_url("asset-does-not-exist-foo.js")
381+
@view.stylesheet_link_tag("asset-does-not-exist-foo.js")
382+
@view.javascript_include_tag("asset-does-not-exist-foo.js")
383+
end
384+
385+
def test_asset_not_precompiled_error
386+
@view.raise_runtime_errors = true
387+
@view.precompile = [ lambda {|logical_path| false } ]
388+
@view.assets_environment = @assets
389+
@view.debug_assets = true
390+
391+
assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
392+
@view.asset_path("foo.js")
393+
end
394+
395+
assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
396+
@view.asset_url("foo.js")
397+
end
398+
399+
assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
400+
@view.javascript_include_tag("foo.js")
401+
end
402+
403+
assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
404+
@view.javascript_include_tag("foo")
405+
end
406+
407+
error = assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
408+
@view.javascript_include_tag(:foo)
409+
end
410+
411+
assert_raise(Sprockets::Rails::Helper::AssetFilteredError) do
412+
@view.stylesheet_link_tag("foo.js")
413+
end
414+
415+
@view.precompile = [ lambda {|logical_path| true } ]
416+
417+
@view.asset_path("foo.js")
418+
@view.asset_url("foo.js")
419+
@view.javascript_include_tag("foo.js")
420+
@view.javascript_include_tag("foo")
421+
@view.javascript_include_tag(:foo)
422+
@view.stylesheet_link_tag("foo.js")
423+
end
424+
375425
def test_asset_digest_path
376426
assert_equal "foo-#{@foo_js_digest}.js", @view.asset_digest_path("foo.js")
377427
assert_equal "foo-#{@foo_css_digest}.css", @view.asset_digest_path("foo.css")
@@ -384,13 +434,17 @@ def test_asset_digest
384434
end
385435

386436
class ErrorsInHelpersTest < HelperTest
437+
387438
def test_dependency_error
388439
@view.raise_runtime_errors = true
440+
@view.precompile = [ lambda {|logical_path| true } ]
441+
@view.assets_environment = @assets
442+
389443
assert_raise Sprockets::Rails::Helper::DependencyError do
390-
@assets['error/dependency.js'].to_s
444+
@view.asset_path("error/dependency.js")
391445
end
392446

393447
@view.raise_runtime_errors = false
394-
@assets['error/dependency.js'].to_s
448+
@view.asset_path("error/dependency.js")
395449
end
396-
end
450+
end

0 commit comments

Comments
 (0)