hanami-sprockets 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.md +21 -0
- data/README.md +234 -0
- data/hanami-sprockets.gemspec +36 -0
- data/lib/hanami/sprockets/asset.rb +119 -0
- data/lib/hanami/sprockets/base_url.rb +62 -0
- data/lib/hanami/sprockets/config.rb +123 -0
- data/lib/hanami/sprockets/errors.rb +29 -0
- data/lib/hanami/sprockets/helpers.rb +206 -0
- data/lib/hanami/sprockets/middleware.rb +68 -0
- data/lib/hanami/sprockets/version.rb +9 -0
- data/lib/hanami/sprockets.rb +338 -0
- data/lib/hanami-sprockets.rb +3 -0
- metadata +229 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hanami/view/html"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
class Assets
|
7
|
+
# Asset helpers for use in templates
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
# @since 0.1.0
|
11
|
+
module Helpers
|
12
|
+
# Generate a stylesheet link tag
|
13
|
+
#
|
14
|
+
# @param sources [Array<String>] one or more stylesheet sources
|
15
|
+
# @param options [Hash] HTML attributes
|
16
|
+
#
|
17
|
+
# @return [String] HTML link tags
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
# @since 0.1.0
|
21
|
+
def stylesheet_tag(*sources, **options)
|
22
|
+
sources.map do |source|
|
23
|
+
if external_source?(source)
|
24
|
+
stylesheet_link_tag(source, **options)
|
25
|
+
else
|
26
|
+
begin
|
27
|
+
asset = hanami_assets[source + ".css"]
|
28
|
+
attrs = build_stylesheet_attributes(asset, **options)
|
29
|
+
stylesheet_link_tag(asset.url, **attrs)
|
30
|
+
rescue AssetMissingError
|
31
|
+
stylesheet_link_tag("#{hanami_assets.config.path_prefix}/#{source}.css", **options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end.join("\n").html_safe
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate a javascript script tag
|
38
|
+
#
|
39
|
+
# @param sources [Array<String>] one or more javascript sources
|
40
|
+
# @param options [Hash] HTML attributes
|
41
|
+
#
|
42
|
+
# @return [String] HTML script tags
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
# @since 0.1.0
|
46
|
+
def javascript_tag(*sources, **options)
|
47
|
+
sources.map do |source|
|
48
|
+
if external_source?(source)
|
49
|
+
javascript_include_tag(source, **options)
|
50
|
+
else
|
51
|
+
begin
|
52
|
+
asset = hanami_assets[source + ".js"]
|
53
|
+
attrs = build_javascript_attributes(asset, **options)
|
54
|
+
javascript_include_tag(asset.url, **attrs)
|
55
|
+
rescue AssetMissingError
|
56
|
+
javascript_include_tag("#{hanami_assets.config.path_prefix}/#{source}.js", **options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end.join("\n").html_safe
|
60
|
+
end
|
61
|
+
|
62
|
+
# Generate an image tag
|
63
|
+
#
|
64
|
+
# @param source [String] image source
|
65
|
+
# @param options [Hash] HTML attributes
|
66
|
+
#
|
67
|
+
# @return [String] HTML img tag
|
68
|
+
#
|
69
|
+
# @api public
|
70
|
+
# @since 0.1.0
|
71
|
+
def image_tag(source, **options)
|
72
|
+
if external_source?(source)
|
73
|
+
build_image_tag(source, **options).html_safe
|
74
|
+
else
|
75
|
+
begin
|
76
|
+
# Try common image extensions
|
77
|
+
%w[.png .jpg .jpeg .gif .svg].each do |ext|
|
78
|
+
begin
|
79
|
+
asset = hanami_assets[source + ext]
|
80
|
+
return build_image_tag(asset.url, **options).html_safe
|
81
|
+
rescue AssetMissingError
|
82
|
+
next
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Fallback to direct path
|
87
|
+
build_image_tag("#{hanami_assets.config.path_prefix}/#{source}", **options).html_safe
|
88
|
+
rescue AssetMissingError
|
89
|
+
build_image_tag("#{hanami_assets.config.path_prefix}/#{source}", **options).html_safe
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get the URL for an asset
|
95
|
+
#
|
96
|
+
# @param source [String] asset source
|
97
|
+
#
|
98
|
+
# @return [String] asset URL
|
99
|
+
#
|
100
|
+
# @api public
|
101
|
+
# @since 0.1.0
|
102
|
+
def asset_url(source)
|
103
|
+
if external_source?(source)
|
104
|
+
source
|
105
|
+
else
|
106
|
+
begin
|
107
|
+
hanami_assets[source].url
|
108
|
+
rescue AssetMissingError
|
109
|
+
"#{hanami_assets.config.path_prefix}/#{source}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the path for an asset (without base URL)
|
115
|
+
#
|
116
|
+
# @param source [String] asset source
|
117
|
+
#
|
118
|
+
# @return [String] asset path
|
119
|
+
#
|
120
|
+
# @api public
|
121
|
+
# @since 0.1.0
|
122
|
+
def asset_path(source)
|
123
|
+
if external_source?(source)
|
124
|
+
source
|
125
|
+
else
|
126
|
+
begin
|
127
|
+
hanami_assets[source].path
|
128
|
+
rescue AssetMissingError
|
129
|
+
"#{hanami_assets.config.path_prefix}/#{source}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def hanami_assets
|
138
|
+
# This should be set by the framework integration
|
139
|
+
@hanami_assets or raise "Hanami::Assets instance not configured"
|
140
|
+
end
|
141
|
+
|
142
|
+
def external_source?(source)
|
143
|
+
source.start_with?('http://') || source.start_with?('https://') || source.start_with?('//')
|
144
|
+
end
|
145
|
+
|
146
|
+
def stylesheet_link_tag(href, **options)
|
147
|
+
attrs = { href: href, type: "text/css", rel: "stylesheet" }.merge(options)
|
148
|
+
"<link#{build_html_attributes(attrs)}>"
|
149
|
+
end
|
150
|
+
|
151
|
+
def javascript_include_tag(src, **options)
|
152
|
+
attrs = { src: src, type: "text/javascript" }.merge(options)
|
153
|
+
"<script#{build_html_attributes(attrs)}></script>"
|
154
|
+
end
|
155
|
+
|
156
|
+
def build_image_tag(src, **options)
|
157
|
+
attrs = { src: src }.merge(options)
|
158
|
+
"<img#{build_html_attributes(attrs)}>"
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
def build_stylesheet_attributes(asset, **options)
|
163
|
+
attrs = options.dup
|
164
|
+
|
165
|
+
if hanami_assets.subresource_integrity? && asset.sri && hanami_assets.crossorigin?(asset.url)
|
166
|
+
attrs[:integrity] = asset.sri
|
167
|
+
attrs[:crossorigin] = "anonymous" unless attrs.key?(:crossorigin)
|
168
|
+
end
|
169
|
+
|
170
|
+
attrs
|
171
|
+
end
|
172
|
+
|
173
|
+
def build_javascript_attributes(asset, **options)
|
174
|
+
attrs = options.dup
|
175
|
+
|
176
|
+
if hanami_assets.subresource_integrity? && asset.sri && hanami_assets.crossorigin?(asset.url)
|
177
|
+
attrs[:integrity] = asset.sri
|
178
|
+
attrs[:crossorigin] = "anonymous" unless attrs.key?(:crossorigin)
|
179
|
+
end
|
180
|
+
|
181
|
+
attrs
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
def build_html_attributes(attrs)
|
186
|
+
attrs.map do |key, value|
|
187
|
+
if value == true
|
188
|
+
" #{key}"
|
189
|
+
elsif value
|
190
|
+
" #{key}=\"#{escape_html(value)}\""
|
191
|
+
end
|
192
|
+
end.join
|
193
|
+
end
|
194
|
+
|
195
|
+
def escape_html(string)
|
196
|
+
string.to_s.gsub(/[&<>"]/, {
|
197
|
+
"&" => "&",
|
198
|
+
"<" => "<",
|
199
|
+
">" => ">",
|
200
|
+
'"' => """
|
201
|
+
})
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class Assets
|
5
|
+
# Rack middleware for serving assets in development
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
# @since 0.1.0
|
9
|
+
class Middleware
|
10
|
+
# @api private
|
11
|
+
# @since 0.1.0
|
12
|
+
def initialize(app, assets)
|
13
|
+
@app = app
|
14
|
+
@assets = assets
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
# @since 0.1.0
|
19
|
+
def call(env)
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
|
22
|
+
# Check if this is an asset request
|
23
|
+
if asset_request?(request.path)
|
24
|
+
serve_asset(request.path)
|
25
|
+
else
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def asset_request?(path)
|
33
|
+
path.start_with?(@assets.config.path_prefix)
|
34
|
+
end
|
35
|
+
|
36
|
+
def serve_asset(path)
|
37
|
+
# Remove the path prefix to get the logical path
|
38
|
+
logical_path = path.sub(@assets.config.path_prefix + "/", "")
|
39
|
+
|
40
|
+
begin
|
41
|
+
# Try to find the asset first
|
42
|
+
sprockets_asset = @assets.environment.find_asset(logical_path)
|
43
|
+
|
44
|
+
# If not found and looks like a fingerprinted asset, try stripping the fingerprint
|
45
|
+
if !sprockets_asset && logical_path.match(/-[a-f0-9]+(\.[^.]+)$/)
|
46
|
+
base_name = logical_path.sub(/-[a-f0-9]+(\.[^.]+)$/, '\1')
|
47
|
+
sprockets_asset = @assets.environment.find_asset(base_name)
|
48
|
+
logical_path = base_name if sprockets_asset
|
49
|
+
end
|
50
|
+
if sprockets_asset
|
51
|
+
headers = {
|
52
|
+
'Content-Type' => sprockets_asset.content_type,
|
53
|
+
'Content-Length' => sprockets_asset.bytesize.to_s,
|
54
|
+
'ETag' => %("#{sprockets_asset.etag}"),
|
55
|
+
'Cache-Control' => 'public, max-age=31536000'
|
56
|
+
}
|
57
|
+
|
58
|
+
[200, headers, [sprockets_asset.source]]
|
59
|
+
else
|
60
|
+
[404, { 'Content-Type' => 'text/plain' }, ['Asset not found']]
|
61
|
+
end
|
62
|
+
rescue => e
|
63
|
+
[500, { 'Content-Type' => 'text/plain' }, ["Asset error: #{e.message}"]]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,338 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "pathname"
|
5
|
+
require "zeitwerk"
|
6
|
+
require "sprockets"
|
7
|
+
require "base64"
|
8
|
+
require "digest"
|
9
|
+
|
10
|
+
module Hanami
|
11
|
+
# Assets management for Ruby web applications using Sprockets
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
class Assets
|
15
|
+
# @since 0.1.0
|
16
|
+
# @api private
|
17
|
+
def self.gem_loader
|
18
|
+
@gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
|
19
|
+
root = File.expand_path("..", __dir__)
|
20
|
+
loader.tag = "hanami-sprockets"
|
21
|
+
loader.push_dir(root)
|
22
|
+
loader.ignore(
|
23
|
+
"#{root}/hanami-sprockets.rb",
|
24
|
+
"#{root}/hanami/sprockets/version.rb",
|
25
|
+
"#{root}/hanami/sprockets/errors.rb"
|
26
|
+
)
|
27
|
+
loader.enable_reloading if loader.respond_to?(:enable_reloading)
|
28
|
+
loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-sprockets.rb")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
gem_loader.setup
|
33
|
+
require_relative "sprockets/version"
|
34
|
+
require_relative "sprockets/errors"
|
35
|
+
require_relative "sprockets/config"
|
36
|
+
require_relative "sprockets/base_url"
|
37
|
+
require_relative "sprockets/asset"
|
38
|
+
require_relative "sprockets/helpers"
|
39
|
+
require_relative "sprockets/middleware"
|
40
|
+
|
41
|
+
# Returns the directory (under `public/assets/`) to be used for storing a slice's compiled
|
42
|
+
# assets.
|
43
|
+
#
|
44
|
+
# This is shared logic used by both Hanami (for the assets provider) and Hanami::CLI (for the
|
45
|
+
# assets commands).
|
46
|
+
#
|
47
|
+
# @since 0.1.0
|
48
|
+
# @api private
|
49
|
+
def self.public_assets_dir(slice)
|
50
|
+
return nil if slice.app.eql?(slice)
|
51
|
+
|
52
|
+
slice.slice_name.to_s.split("/").map { |name| "_#{name}" }.join("/")
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
# @since 0.1.0
|
57
|
+
attr_reader :config
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
# @since 0.1.0
|
61
|
+
attr_reader :root
|
62
|
+
|
63
|
+
# @api private
|
64
|
+
# @since 0.1.0
|
65
|
+
attr_reader :environment
|
66
|
+
|
67
|
+
# @api public
|
68
|
+
# @since 0.1.0
|
69
|
+
def initialize(config:, root:)
|
70
|
+
@config = config
|
71
|
+
@root = Pathname(root)
|
72
|
+
@environment = setup_sprockets_environment
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the asset at the given path.
|
76
|
+
#
|
77
|
+
# @return [Hanami::Sprockets::Asset] the asset
|
78
|
+
#
|
79
|
+
# @raise AssetMissingError if no asset can be found at the path
|
80
|
+
#
|
81
|
+
# @api public
|
82
|
+
# @since 0.1.0
|
83
|
+
def [](path)
|
84
|
+
# Find the asset using Sprockets
|
85
|
+
sprockets_asset = environment.find_asset(path)
|
86
|
+
|
87
|
+
raise AssetMissingError.new(path) unless sprockets_asset
|
88
|
+
|
89
|
+
# Generate the asset path - use digest_path for fingerprinting in production
|
90
|
+
asset_path = if config.digest
|
91
|
+
"#{config.path_prefix}/#{sprockets_asset.digest_path}"
|
92
|
+
else
|
93
|
+
"#{config.path_prefix}/#{sprockets_asset.logical_path}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Create our Asset wrapper
|
97
|
+
Asset.new(
|
98
|
+
path: asset_path,
|
99
|
+
base_url: config.base_url,
|
100
|
+
sri: calculate_sri(sprockets_asset),
|
101
|
+
logical_path: sprockets_asset.logical_path,
|
102
|
+
digest_path: sprockets_asset.digest_path,
|
103
|
+
content_type: sprockets_asset.content_type,
|
104
|
+
source: sprockets_asset.source
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true if subresource integrity is configured.
|
109
|
+
#
|
110
|
+
# @return [Boolean]
|
111
|
+
#
|
112
|
+
# @api public
|
113
|
+
# @since 0.1.0
|
114
|
+
def subresource_integrity?
|
115
|
+
config.subresource_integrity.any?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns true if the given source path is a cross-origin request.
|
119
|
+
#
|
120
|
+
# @return [Boolean]
|
121
|
+
#
|
122
|
+
# @api public
|
123
|
+
# @since 0.1.0
|
124
|
+
def crossorigin?(source_path)
|
125
|
+
config.crossorigin?(source_path)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Precompile assets (for production)
|
129
|
+
#
|
130
|
+
# @api public
|
131
|
+
# @since 0.1.0
|
132
|
+
def precompile(target_dir = nil, &block)
|
133
|
+
target_dir ||= root.join("public", "assets")
|
134
|
+
target_dir = Pathname(target_dir)
|
135
|
+
target_dir.mkpath
|
136
|
+
|
137
|
+
manifest = ::Sprockets::Manifest.new(environment, target_dir, "manifest.json")
|
138
|
+
|
139
|
+
# Precompile configured assets - compile by explicit names first
|
140
|
+
['app.css', 'app.js'].each do |asset|
|
141
|
+
begin
|
142
|
+
manifest.compile(asset)
|
143
|
+
block&.call(asset) if block
|
144
|
+
rescue Sprockets::FileNotFound
|
145
|
+
# Asset doesn't exist, skip it
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Then process configured precompile patterns
|
150
|
+
config.precompile.each do |asset|
|
151
|
+
begin
|
152
|
+
manifest.compile(asset)
|
153
|
+
block&.call(asset) if block
|
154
|
+
rescue Sprockets::FileNotFound
|
155
|
+
# Asset doesn't exist, skip it
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Write the manifest file
|
160
|
+
File.write(File.join(target_dir, "manifest.json"), JSON.pretty_generate(manifest.assets))
|
161
|
+
|
162
|
+
manifest
|
163
|
+
end
|
164
|
+
|
165
|
+
# Get all logical paths (useful for debugging)
|
166
|
+
#
|
167
|
+
# @api public
|
168
|
+
# @since 0.1.0
|
169
|
+
def logical_paths
|
170
|
+
paths = []
|
171
|
+
# Walk through all load paths and find assets
|
172
|
+
environment.paths.each do |load_path|
|
173
|
+
next unless Dir.exist?(load_path)
|
174
|
+
|
175
|
+
Dir.glob("**/*", base: load_path).each do |file|
|
176
|
+
full_path = File.join(load_path, file)
|
177
|
+
next unless File.file?(full_path)
|
178
|
+
next if File.basename(file).start_with?(".")
|
179
|
+
|
180
|
+
# Try to find it as an asset to see if Sprockets can handle it
|
181
|
+
begin
|
182
|
+
if environment.find_asset(file)
|
183
|
+
paths << file
|
184
|
+
end
|
185
|
+
rescue
|
186
|
+
# Skip files that cause errors
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
paths.uniq.sort
|
192
|
+
end
|
193
|
+
# Clear the cache (useful in development)
|
194
|
+
#
|
195
|
+
# @api public
|
196
|
+
# @since 0.1.0
|
197
|
+
def clear_cache!
|
198
|
+
@environment = setup_sprockets_environment
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
def setup_sprockets_environment
|
204
|
+
env = ::Sprockets::Environment.new(root.to_s)
|
205
|
+
|
206
|
+
# Set up context class for helpers
|
207
|
+
assets_config = config
|
208
|
+
env.context_class.class_eval do
|
209
|
+
define_method :asset_path do |path, options = {}|
|
210
|
+
# Find the asset and return its path
|
211
|
+
asset = environment.find_asset(path)
|
212
|
+
if asset
|
213
|
+
if assets_config.digest
|
214
|
+
"#{assets_config.path_prefix}/#{asset.digest_path}"
|
215
|
+
else
|
216
|
+
"#{assets_config.path_prefix}/#{asset.logical_path}"
|
217
|
+
end
|
218
|
+
else
|
219
|
+
"#{assets_config.path_prefix}/#{path}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
define_method :asset_url do |path, options = {}|
|
224
|
+
asset_path(path, options)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Add common Rails-like asset paths
|
229
|
+
potential_paths = [
|
230
|
+
"app/assets/stylesheets",
|
231
|
+
"app/assets/javascripts",
|
232
|
+
"app/assets/images",
|
233
|
+
"app/assets/fonts",
|
234
|
+
"lib/assets/stylesheets",
|
235
|
+
"lib/assets/javascripts",
|
236
|
+
"lib/assets/images",
|
237
|
+
"vendor/assets/stylesheets",
|
238
|
+
"vendor/assets/javascripts",
|
239
|
+
"vendor/assets/images"
|
240
|
+
]
|
241
|
+
|
242
|
+
potential_paths.each do |path_str|
|
243
|
+
full_path = root.join(path_str)
|
244
|
+
env.append_path(full_path.to_s) if full_path.exist?
|
245
|
+
end
|
246
|
+
|
247
|
+
# Add any additional paths from config
|
248
|
+
config.asset_paths.each { |path| env.append_path(path) }
|
249
|
+
|
250
|
+
# Automatically discover and add gem asset paths
|
251
|
+
discover_gem_asset_paths.each { |path| env.append_path(path) }
|
252
|
+
|
253
|
+
# Configure processors based on what gems are available
|
254
|
+
configure_processors(env)
|
255
|
+
|
256
|
+
env
|
257
|
+
end
|
258
|
+
|
259
|
+
def configure_processors(env)
|
260
|
+
# Sprockets will auto-detect most processors if the gems are available
|
261
|
+
# But we can explicitly configure them here if needed
|
262
|
+
|
263
|
+
# SCSS/Sass support (if sassc is available)
|
264
|
+
begin
|
265
|
+
require 'sassc'
|
266
|
+
# SassC will automatically be used for .scss and .sass files
|
267
|
+
# Sprockets will handle this automatically
|
268
|
+
rescue LoadError
|
269
|
+
# Sass not available, that's fine
|
270
|
+
end
|
271
|
+
|
272
|
+
# CoffeeScript support (if coffee-rails is available)
|
273
|
+
begin
|
274
|
+
require 'coffee_script'
|
275
|
+
# Sprockets will automatically use CoffeeScript if available
|
276
|
+
rescue LoadError
|
277
|
+
# CoffeeScript not available, that's fine
|
278
|
+
end
|
279
|
+
|
280
|
+
# ES6+ support via Babel (if babel-transpiler is available)
|
281
|
+
begin
|
282
|
+
require 'babel/transpiler'
|
283
|
+
env.register_transformer 'application/javascript', 'application/javascript', Sprockets::BabelProcessor
|
284
|
+
rescue LoadError
|
285
|
+
# Babel not available, that's fine
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def discover_gem_asset_paths
|
290
|
+
paths = []
|
291
|
+
|
292
|
+
# Common asset directory patterns that gems use
|
293
|
+
asset_patterns = [
|
294
|
+
'assets/stylesheets',
|
295
|
+
'assets/javascripts',
|
296
|
+
'assets/images',
|
297
|
+
'assets/fonts',
|
298
|
+
'app/assets/stylesheets',
|
299
|
+
'app/assets/javascripts',
|
300
|
+
'app/assets/images',
|
301
|
+
'vendor/assets/stylesheets',
|
302
|
+
'vendor/assets/javascripts'
|
303
|
+
]
|
304
|
+
|
305
|
+
# Iterate through all loaded gems
|
306
|
+
Gem.loaded_specs.each do |name, spec|
|
307
|
+
asset_patterns.each do |pattern|
|
308
|
+
potential_path = File.join(spec.gem_dir, pattern)
|
309
|
+
if File.directory?(potential_path)
|
310
|
+
paths << potential_path
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
paths
|
316
|
+
end
|
317
|
+
|
318
|
+
def calculate_sri(sprockets_asset)
|
319
|
+
return nil unless subresource_integrity?
|
320
|
+
|
321
|
+
algorithms = config.subresource_integrity
|
322
|
+
sri_values = []
|
323
|
+
|
324
|
+
algorithms.each do |algorithm|
|
325
|
+
case algorithm
|
326
|
+
when :sha256
|
327
|
+
sri_values << "sha256-#{Base64.strict_encode64(Digest::SHA256.digest(sprockets_asset.source))}"
|
328
|
+
when :sha384
|
329
|
+
sri_values << "sha384-#{Base64.strict_encode64(Digest::SHA384.digest(sprockets_asset.source))}"
|
330
|
+
when :sha512
|
331
|
+
sri_values << "sha512-#{Base64.strict_encode64(Digest::SHA512.digest(sprockets_asset.source))}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
sri_values.join(" ")
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|