Module: Senren::Rails::AssetPathGuard
- Defined in:
- lib/senren/rails/asset_path_guard.rb
Overview
Refuses to let component source be served as a static asset.
ViewComponent documents putting sidecar assets next to components, and the usual way to reach them is:
config.assets.paths << Rails.root.join("app/components")
With Propshaft that one line makes every file under app/components a
servable asset, .rb and .html.erb included. Verified against Propshaft
1.3.2: 129 component source files resolved, assets:precompile copied all
of them into public/assets/, and public/assets/.manifest.json listed each
logical path next to its digested filename — so the digest is not even an
obstacle. In production the web server hands them out with Rails never
involved.
Development is a different risk calculation, so there it warns. Production raises: a boot failure is recoverable, published source is not.
Constant Summary collapse
- SOURCE_EXTENSIONS =
%w[.rb .erb].freeze
Class Method Summary collapse
- .asset_paths(app) ⇒ Object
-
.check!(app, io: $stderr, production: production_env?) ) ⇒ Object
production:is injected rather than read from ::Rails so the guard can be unit tested without booting Rails, which is how the rest of this library's unit suite runs. - .components_dir(app) ⇒ Object
- .message_for(offenders) ⇒ Object
-
.offending_paths(app) ⇒ Object
An asset path is only a problem when component source actually sits under it, so an app that keeps sidecar assets in their own directory is left alone.
-
.overlap?(one, other) ⇒ Boolean
Two paths overlap when one contains the other, in EITHER direction, and the test has to be separator-aware:.
-
.production_env? ⇒ Boolean
Fails closed.
-
.publishes_source?(asset_path, components) ⇒ Boolean
Whether serving this asset path would serve component source.
- .source_files?(dir) ⇒ Boolean
Class Method Details
.asset_paths(app) ⇒ Object
90 91 92 93 94 |
# File 'lib/senren/rails/asset_path_guard.rb', line 90 def asset_paths(app) app.config.respond_to?(:assets) ? Array(app.config.assets.paths) : [] rescue StandardError [] end |
.check!(app, io: $stderr, production: production_env?) ) ⇒ Object
production: is injected rather than read from ::Rails so the guard can
be unit tested without booting Rails, which is how the rest of this
library's unit suite runs.
30 31 32 33 34 35 36 37 38 39 |
# File 'lib/senren/rails/asset_path_guard.rb', line 30 def check!(app, io: $stderr, production: production_env?) offenders = offending_paths(app) return true if offenders.empty? = (offenders) raise if production io.puts("[senren] WARNING: #{}") false end |
.components_dir(app) ⇒ Object
80 81 82 83 84 |
# File 'lib/senren/rails/asset_path_guard.rb', line 80 def components_dir(app) app.root.join('app/components') rescue StandardError nil end |
.message_for(offenders) ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/senren/rails/asset_path_guard.rb', line 114 def (offenders) <<~MESSAGE.strip app/components is on the asset load path (#{offenders.join(', ')}). Propshaft serves every file under an asset path, so your component .rb and .html.erb source would be published — assets:precompile copies them into public/assets and .manifest.json lists them by name. Move sidecar assets into their own directory and add that instead, for example app/components/assets rather than app/components. MESSAGE end |
.offending_paths(app) ⇒ Object
An asset path is only a problem when component source actually sits under it, so an app that keeps sidecar assets in their own directory is left alone.
57 58 59 60 61 62 |
# File 'lib/senren/rails/asset_path_guard.rb', line 57 def offending_paths(app) components = components_dir(app) return [] unless components&.directory? asset_paths(app).select { |path| publishes_source?(path, components) } end |
.overlap?(one, other) ⇒ Boolean
Two paths overlap when one contains the other, in EITHER direction, and the test has to be separator-aware:
app/components is an ancestor -> publishes everything
app/components/senren is a descendant -> publishes the components
app/comp shares a prefix -> publishes nothing
The first version tested only the ancestor direction with a bare start_with?, so it missed the descendant case — which is what a developer writes after reading this module's own remediation text — and it blocked production boots over an unrelated app/comp directory.
107 108 109 110 111 112 |
# File 'lib/senren/rails/asset_path_guard.rb', line 107 def overlap?(one, other) a = "#{one}#{File::SEPARATOR}" b = "#{other}#{File::SEPARATOR}" a.start_with?(b) || b.start_with?(a) end |
.production_env? ⇒ Boolean
Fails closed. Rails.env.production? let a conventional
RAILS_ENV=staging deploy print one line of stderr and precompile the
source anyway, and the same held for review apps and any custom
environment name. local? is true only for development and test — the
two environments where exposure is acceptable — so everything else is
treated as deployed. Available since Rails 7.1, which is the floor.
47 48 49 50 51 52 |
# File 'lib/senren/rails/asset_path_guard.rb', line 47 def production_env? return false unless defined?(::Rails) && ::Rails.respond_to?(:env) return !::Rails.env.local? if ::Rails.env.respond_to?(:local?) !%w[development test].include?(::Rails.env.to_s) end |
.publishes_source?(asset_path, components) ⇒ Boolean
Whether serving this asset path would serve component source.
source_files? used to be asked once about app/components as a whole,
which is the wrong question for a descendant path: with that shape,
app/components/assets holding nothing but CSS looked identical to
app/components/senren holding every component. Only the overlapping
subtree can publish anything, and that subtree is the deeper of the two
paths.
72 73 74 75 76 77 78 |
# File 'lib/senren/rails/asset_path_guard.rb', line 72 def publishes_source?(asset_path, components) asset = Pathname.new(asset_path.to_s). components = components. return false unless overlap?(asset, components) source_files?(asset.to_s.length >= components.to_s.length ? asset : components) end |
.source_files?(dir) ⇒ Boolean
86 87 88 |
# File 'lib/senren/rails/asset_path_guard.rb', line 86 def source_files?(dir) SOURCE_EXTENSIONS.any? { |ext| Dir.glob(dir.join("**/*#{ext}")).any? } end |