hanami-assets 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +49 -25
- data/bin/hanami-assets +4 -4
- data/hanami-assets.gemspec +7 -7
- data/lib/hanami/assets/bundler/asset.rb +98 -0
- data/lib/hanami/assets/bundler/compressor.rb +61 -0
- data/lib/hanami/assets/bundler/manifest_entry.rb +62 -0
- data/lib/hanami/assets/bundler.rb +35 -62
- data/lib/hanami/assets/cache.rb +58 -15
- data/lib/hanami/assets/compiler.rb +66 -45
- data/lib/hanami/assets/compilers/less.rb +29 -0
- data/lib/hanami/assets/compilers/sass.rb +46 -0
- data/lib/hanami/assets/compressors/sass_stylesheet.rb +2 -2
- data/lib/hanami/assets/config/global_sources.rb +1 -1
- data/lib/hanami/assets/config/manifest.rb +23 -3
- data/lib/hanami/assets/config/sources.rb +8 -3
- data/lib/hanami/assets/configuration.rb +90 -23
- data/lib/hanami/assets/helpers.rb +81 -9
- data/lib/hanami/assets/precompiler.rb +31 -8
- data/lib/hanami/assets/version.rb +1 -1
- data/lib/hanami/assets.rb +12 -10
- metadata +15 -11
data/lib/hanami/assets/cache.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'pathname'
|
2
3
|
|
3
4
|
module Hanami
|
4
5
|
module Assets
|
@@ -10,25 +11,75 @@ module Hanami
|
|
10
11
|
# @since 0.1.0
|
11
12
|
# @api private
|
12
13
|
class Cache
|
14
|
+
# File cache entry
|
15
|
+
#
|
16
|
+
# @since 0.3.0
|
17
|
+
# @api private
|
18
|
+
class File
|
19
|
+
# @since 0.3.0
|
20
|
+
# @api private
|
21
|
+
def initialize(file, mtime: nil, dependencies: nil)
|
22
|
+
@file = file.is_a?(String) ? Pathname.new(file) : file
|
23
|
+
@mtime = mtime || @file.mtime.utc.to_i
|
24
|
+
|
25
|
+
@dependencies = (dependencies || []).map { |d| self.class.new(d) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# @since 0.3.0
|
29
|
+
# @api private
|
30
|
+
def modified?(other)
|
31
|
+
file = other.is_a?(self.class) ? other : self.class.new(other)
|
32
|
+
|
33
|
+
if dependencies?
|
34
|
+
modified_dependencies?(file) ||
|
35
|
+
mtime <= file.mtime
|
36
|
+
else
|
37
|
+
mtime < file.mtime
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# @since 0.3.0
|
44
|
+
# @api private
|
45
|
+
attr_reader :mtime
|
46
|
+
|
47
|
+
# @since 0.3.0
|
48
|
+
# @api private
|
49
|
+
attr_reader :dependencies
|
50
|
+
|
51
|
+
# @since 0.3.0
|
52
|
+
# @api private
|
53
|
+
def modified_dependencies?(other)
|
54
|
+
dependencies.all? { |dep| dep.mtime > other.mtime }
|
55
|
+
end
|
56
|
+
|
57
|
+
# @since 0.3.0
|
58
|
+
# @api private
|
59
|
+
def dependencies?
|
60
|
+
dependencies.any?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
13
64
|
# Return a new instance
|
14
65
|
#
|
15
66
|
# @return [Hanami::Assets::Cache] a new instance
|
16
67
|
def initialize
|
17
|
-
@data = Hash.new{|h,k| h[k] = 0 }
|
68
|
+
@data = Hash.new { |h, k| h[k] = File.new(k, mtime: 0) }
|
18
69
|
@mutex = Mutex.new
|
19
70
|
end
|
20
71
|
|
21
|
-
# Check if the given file
|
72
|
+
# Check if the given file was modified
|
22
73
|
#
|
23
74
|
# @param file [String,Pathname] the file path
|
24
75
|
#
|
25
76
|
# @return [TrueClass,FalseClass] the result of the check
|
26
77
|
#
|
27
|
-
# @since 0.
|
78
|
+
# @since 0.3.0
|
28
79
|
# @api private
|
29
|
-
def
|
80
|
+
def modified?(file)
|
30
81
|
@mutex.synchronize do
|
31
|
-
@data[file.to_s]
|
82
|
+
@data[file.to_s].modified?(file)
|
32
83
|
end
|
33
84
|
end
|
34
85
|
|
@@ -40,19 +91,11 @@ module Hanami
|
|
40
91
|
#
|
41
92
|
# @since 0.1.0
|
42
93
|
# @api private
|
43
|
-
def store(file)
|
94
|
+
def store(file, dependencies = nil)
|
44
95
|
@mutex.synchronize do
|
45
|
-
@data[file.to_s] =
|
96
|
+
@data[file.to_s] = File.new(file, dependencies: dependencies)
|
46
97
|
end
|
47
98
|
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# @since 0.1.0
|
52
|
-
# @api private
|
53
|
-
def mtime(file)
|
54
|
-
file.mtime.utc.to_i
|
55
|
-
end
|
56
99
|
end
|
57
100
|
end
|
58
101
|
end
|
@@ -1,17 +1,21 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'find'
|
3
|
+
require 'hanami/utils/class_attribute'
|
2
4
|
|
3
5
|
module Hanami
|
4
6
|
module Assets
|
7
|
+
# Missing Asset error
|
5
8
|
class MissingAsset < Error
|
6
9
|
def initialize(name, sources)
|
7
10
|
sources = sources.map(&:to_s).join(', ')
|
8
|
-
super("Missing asset: `#{
|
11
|
+
super("Missing asset: `#{name}' (sources: #{sources})")
|
9
12
|
end
|
10
13
|
end
|
11
14
|
|
15
|
+
# Unknown Asset Engine error
|
12
16
|
class UnknownAssetEngine < Error
|
13
17
|
def initialize(source)
|
14
|
-
super("No asset engine registered for `#{
|
18
|
+
super("No asset engine registered for `#{::File.basename(source)}'")
|
15
19
|
end
|
16
20
|
end
|
17
21
|
|
@@ -24,10 +28,10 @@ module Hanami
|
|
24
28
|
#
|
25
29
|
# @since 0.1.0
|
26
30
|
# @api private
|
27
|
-
class Compiler
|
31
|
+
class Compiler # rubocop:disable Metrics/ClassLength
|
28
32
|
# @since 0.1.0
|
29
33
|
# @api private
|
30
|
-
DEFAULT_PERMISSIONS =
|
34
|
+
DEFAULT_PERMISSIONS = 0o644
|
31
35
|
|
32
36
|
# @since 0.1.0
|
33
37
|
# @api private
|
@@ -35,12 +39,21 @@ module Hanami
|
|
35
39
|
|
36
40
|
# @since 0.1.0
|
37
41
|
# @api private
|
38
|
-
EXTENSIONS = {'.js' => true, '.css' => true, '.map' => true}.freeze
|
42
|
+
EXTENSIONS = { '.js' => true, '.css' => true, '.map' => true }.freeze
|
39
43
|
|
40
|
-
|
44
|
+
include Utils::ClassAttribute
|
45
|
+
|
46
|
+
# @since 0.3.0
|
41
47
|
# @api private
|
42
|
-
|
43
|
-
|
48
|
+
class_attribute :subclasses
|
49
|
+
self.subclasses = Set.new
|
50
|
+
|
51
|
+
# @since 0.3.0
|
52
|
+
# @api private
|
53
|
+
def self.inherited(subclass)
|
54
|
+
super
|
55
|
+
subclasses.add(subclass)
|
56
|
+
end
|
44
57
|
|
45
58
|
# Compile the given asset
|
46
59
|
#
|
@@ -56,7 +69,26 @@ module Hanami
|
|
56
69
|
|
57
70
|
require 'tilt'
|
58
71
|
require 'hanami/assets/cache'
|
59
|
-
|
72
|
+
require 'hanami/assets/compilers/sass'
|
73
|
+
require 'hanami/assets/compilers/less'
|
74
|
+
fabricate(configuration, name).compile
|
75
|
+
end
|
76
|
+
|
77
|
+
# @since 0.3.0
|
78
|
+
# @api private
|
79
|
+
def self.fabricate(configuration, name)
|
80
|
+
source = configuration.source(name)
|
81
|
+
engine = (subclasses + [self]).find do |klass|
|
82
|
+
klass.eligible?(source)
|
83
|
+
end
|
84
|
+
|
85
|
+
engine.new(configuration, name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @since 0.3.0
|
89
|
+
# @api private
|
90
|
+
def self.eligible?(_name)
|
91
|
+
true
|
60
92
|
end
|
61
93
|
|
62
94
|
# Assets cache
|
@@ -66,7 +98,7 @@ module Hanami
|
|
66
98
|
#
|
67
99
|
# @see Hanami::Assets::Cache
|
68
100
|
def self.cache
|
69
|
-
@@cache ||= Assets::Cache.new
|
101
|
+
@@cache ||= Assets::Cache.new # rubocop:disable Style/ClassVars
|
70
102
|
end
|
71
103
|
|
72
104
|
# Return a new instance
|
@@ -94,7 +126,7 @@ module Hanami
|
|
94
126
|
# @api private
|
95
127
|
def compile
|
96
128
|
raise MissingAsset.new(@name, @configuration.sources) unless exist?
|
97
|
-
return unless
|
129
|
+
return unless modified?
|
98
130
|
|
99
131
|
if compile?
|
100
132
|
compile!
|
@@ -110,10 +142,7 @@ module Hanami
|
|
110
142
|
# @since 0.1.0
|
111
143
|
# @api private
|
112
144
|
def source
|
113
|
-
@source ||=
|
114
|
-
@name.absolute? ? @name :
|
115
|
-
@configuration.find(@name)
|
116
|
-
end
|
145
|
+
@source ||= @configuration.source(@name)
|
117
146
|
end
|
118
147
|
|
119
148
|
# @since 0.1.0
|
@@ -141,38 +170,24 @@ module Hanami
|
|
141
170
|
source.exist?
|
142
171
|
end
|
143
172
|
|
144
|
-
# @since 0.
|
173
|
+
# @since 0.3.0
|
145
174
|
# @api private
|
146
|
-
def
|
175
|
+
def modified?
|
147
176
|
!destination.exist? ||
|
148
|
-
cache.
|
177
|
+
cache.modified?(source)
|
149
178
|
end
|
150
179
|
|
151
180
|
# @since 0.1.0
|
152
181
|
# @api private
|
153
182
|
def compile?
|
154
|
-
@compile ||= ::File.fnmatch(COMPILE_PATTERN, source.to_s) &&
|
155
|
-
|
183
|
+
@compile ||= ::File.fnmatch(COMPILE_PATTERN, ::File.basename(source.to_s)) &&
|
184
|
+
!EXTENSIONS[::File.extname(source.to_s)]
|
156
185
|
end
|
157
186
|
|
158
187
|
# @since 0.1.0
|
159
188
|
# @api private
|
160
189
|
def compile!
|
161
|
-
|
162
|
-
# For now we don't want to maintan a specialized Compiler version for Sass.
|
163
|
-
#
|
164
|
-
# If in the future other precompilers will need special treatment,
|
165
|
-
# we can consider to maintain several specialized versions in order to
|
166
|
-
# don't add a perf tax to all the other preprocessors who "just work".
|
167
|
-
#
|
168
|
-
# Example: if Less "just works", we can keep it in the general `Compiler',
|
169
|
-
# but have a `SassCompiler` if it requires more than `:load_paths'.
|
170
|
-
#
|
171
|
-
# NOTE: We need another option to pass for Sass: `:cache_location'.
|
172
|
-
#
|
173
|
-
# This is needed to don't create a `.sass-cache' directory at the root of the project,
|
174
|
-
# but to have it under `tmp/sass-cache'.
|
175
|
-
write { Tilt.new(source, nil, load_paths: sass_load_paths, cache_location: sass_cache_location).render }
|
190
|
+
write { renderer.render }
|
176
191
|
rescue RuntimeError
|
177
192
|
raise UnknownAssetEngine.new(source)
|
178
193
|
end
|
@@ -186,14 +201,14 @@ module Hanami
|
|
186
201
|
# @since 0.1.0
|
187
202
|
# @api private
|
188
203
|
def cache!
|
189
|
-
cache.store(source)
|
204
|
+
cache.store(source, dependencies)
|
190
205
|
end
|
191
206
|
|
192
207
|
# @since 0.1.0
|
193
208
|
# @api private
|
194
209
|
def write
|
195
210
|
destination.dirname.mkpath
|
196
|
-
destination.open(File::WRONLY|File::TRUNC|File::CREAT, DEFAULT_PERMISSIONS) do |file|
|
211
|
+
destination.open(File::WRONLY | File::TRUNC | File::CREAT, DEFAULT_PERMISSIONS) do |file|
|
197
212
|
file.write(yield)
|
198
213
|
end
|
199
214
|
end
|
@@ -204,9 +219,21 @@ module Hanami
|
|
204
219
|
self.class.cache
|
205
220
|
end
|
206
221
|
|
207
|
-
# @since
|
222
|
+
# @since 0.3.0
|
223
|
+
# @api private
|
224
|
+
def renderer
|
225
|
+
Tilt.new(source)
|
226
|
+
end
|
227
|
+
|
228
|
+
# @since 0.3.0
|
229
|
+
# @api private
|
230
|
+
def dependencies
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# @since 0.3.0
|
208
235
|
# @api private
|
209
|
-
def
|
236
|
+
def load_paths
|
210
237
|
result = []
|
211
238
|
|
212
239
|
@configuration.sources.each do |source|
|
@@ -217,12 +244,6 @@ module Hanami
|
|
217
244
|
|
218
245
|
result
|
219
246
|
end
|
220
|
-
|
221
|
-
# @since 0.1.0
|
222
|
-
# @api private
|
223
|
-
def sass_cache_location
|
224
|
-
SASS_CACHE_LOCATION
|
225
|
-
end
|
226
247
|
end
|
227
248
|
end
|
228
249
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Assets
|
3
|
+
module Compilers
|
4
|
+
# LESS compiler
|
5
|
+
#
|
6
|
+
# @since 0.3.0
|
7
|
+
# @api private
|
8
|
+
class Less < Compiler
|
9
|
+
# @since 0.3.0
|
10
|
+
# @api private
|
11
|
+
EXTENSIONS = /\.(less)\z/
|
12
|
+
|
13
|
+
# @since 0.3.0
|
14
|
+
# @api private
|
15
|
+
def self.eligible?(name)
|
16
|
+
name.to_s =~ EXTENSIONS
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @since 0.3.0
|
22
|
+
# @api private
|
23
|
+
def renderer
|
24
|
+
Tilt.new(source, nil, paths: load_paths)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Assets
|
3
|
+
module Compilers
|
4
|
+
# Sass/SCSS Compiler
|
5
|
+
#
|
6
|
+
# @since 0.3.0
|
7
|
+
# @api private
|
8
|
+
class Sass < Compiler
|
9
|
+
# @since 0.3.0
|
10
|
+
# @api private
|
11
|
+
EXTENSIONS = /\.(sass|scss)\z/
|
12
|
+
|
13
|
+
# @since 0.1.0
|
14
|
+
# @api private
|
15
|
+
CACHE_LOCATION = Pathname(Hanami.respond_to?(:root) ? # rubocop:disable Style/MultilineTernaryOperator
|
16
|
+
Hanami.root : Dir.pwd).join('tmp', 'sass-cache')
|
17
|
+
|
18
|
+
# @since 0.3.0
|
19
|
+
# @api private
|
20
|
+
def self.eligible?(name)
|
21
|
+
name.to_s =~ EXTENSIONS
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @since 0.3.0
|
27
|
+
# @api private
|
28
|
+
def renderer
|
29
|
+
Tilt.new(source, nil, load_paths: load_paths, cache_location: CACHE_LOCATION)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @since 0.3.0
|
33
|
+
# @api private
|
34
|
+
def dependencies
|
35
|
+
engine.dependencies.map { |d| d.options[:filename] }
|
36
|
+
end
|
37
|
+
|
38
|
+
# @since 0.3.0
|
39
|
+
# @api private
|
40
|
+
def engine
|
41
|
+
::Sass::Engine.for_file(source.to_s, load_paths: load_paths, cache_location: CACHE_LOCATION)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -18,7 +18,7 @@ module Hanami
|
|
18
18
|
# @api private
|
19
19
|
#
|
20
20
|
# FIXME This is the same logic that we have for Hanami::Assets::Compiler
|
21
|
-
SASS_CACHE_LOCATION = Pathname(Hanami.respond_to?(:root) ?
|
21
|
+
SASS_CACHE_LOCATION = Pathname(Hanami.respond_to?(:root) ? # rubocop:disable Style/MultilineTernaryOperator
|
22
22
|
Hanami.root : Dir.pwd).join('tmp', 'sass-cache')
|
23
23
|
# @since 0.1.0
|
24
24
|
# @api private
|
@@ -30,7 +30,7 @@ module Hanami
|
|
30
30
|
# @api private
|
31
31
|
def compress(filename)
|
32
32
|
compressor.new(read(filename), filename: filename, syntax: :scss,
|
33
|
-
|
33
|
+
style: :compressed, cache_location: SASS_CACHE_LOCATION).render
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
@@ -7,7 +7,7 @@ module Hanami
|
|
7
7
|
# @api private
|
8
8
|
class MissingDigestManifestError < Error
|
9
9
|
def initialize(path)
|
10
|
-
super("Can't read manifest: #{
|
10
|
+
super("Can't read manifest: #{path}")
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
@@ -18,7 +18,7 @@ module Hanami
|
|
18
18
|
# @api private
|
19
19
|
class MissingDigestAssetError < Error
|
20
20
|
def initialize(asset, manifest_path)
|
21
|
-
super("Can't find asset `#{
|
21
|
+
super("Can't find asset `#{asset}' in manifest (#{manifest_path})")
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -73,6 +73,14 @@ module Hanami
|
|
73
73
|
# @since 0.1.0
|
74
74
|
# @api private
|
75
75
|
class DigestManifest
|
76
|
+
# @since 0.3.0
|
77
|
+
# @api private
|
78
|
+
TARGET = 'target'.freeze
|
79
|
+
|
80
|
+
# @since 0.3.0
|
81
|
+
# @api private
|
82
|
+
SUBRESOURCE_INTEGRITY = 'sri'.freeze
|
83
|
+
|
76
84
|
# Return a new instance
|
77
85
|
#
|
78
86
|
# @param assets [Hash] the content of the digest manifest
|
@@ -95,7 +103,7 @@ module Hanami
|
|
95
103
|
# For a given path <tt>/assets/application.js</tt> it will return
|
96
104
|
# <tt>/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js</tt>
|
97
105
|
#
|
98
|
-
# @param asset [#to_s] the
|
106
|
+
# @param asset [#to_s] the relative asset path
|
99
107
|
#
|
100
108
|
# @return [String] the digest path
|
101
109
|
#
|
@@ -106,6 +114,18 @@ module Hanami
|
|
106
114
|
raise Hanami::Assets::MissingDigestAssetError.new(asset, @manifest_path)
|
107
115
|
end
|
108
116
|
end
|
117
|
+
|
118
|
+
# @since 0.3.0
|
119
|
+
# @api private
|
120
|
+
def target(path)
|
121
|
+
resolve(path).fetch(TARGET)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @since 0.3.0
|
125
|
+
# @api private
|
126
|
+
def subresource_integrity_values(path)
|
127
|
+
resolve(path).fetch(SUBRESOURCE_INTEGRITY)
|
128
|
+
end
|
109
129
|
end
|
110
130
|
end
|
111
131
|
end
|
@@ -17,6 +17,10 @@ module Hanami
|
|
17
17
|
#
|
18
18
|
# TODO The perf of this class is poor, consider to improve it.
|
19
19
|
class Sources < Utils::LoadPaths
|
20
|
+
# @since 0.3.0
|
21
|
+
# @api private
|
22
|
+
SKIPPED_FILE_PREFIX = '_'.freeze
|
23
|
+
|
20
24
|
# @since 0.1.0
|
21
25
|
# @api private
|
22
26
|
attr_writer :root
|
@@ -31,7 +35,7 @@ module Hanami
|
|
31
35
|
# @since 0.1.0
|
32
36
|
# @api private
|
33
37
|
def map
|
34
|
-
|
38
|
+
[].tap do |result|
|
35
39
|
each do |source|
|
36
40
|
result << yield(source)
|
37
41
|
end
|
@@ -51,8 +55,8 @@ module Hanami
|
|
51
55
|
def files(name = nil)
|
52
56
|
result = []
|
53
57
|
|
54
|
-
Dir.glob(map {|source| "#{
|
55
|
-
next if ::File.directory?(file) || ::File.basename(file).
|
58
|
+
Dir.glob(map { |source| "#{source}#{::File::SEPARATOR}**#{::File::SEPARATOR}#{name}*" }).each do |file|
|
59
|
+
next if ::File.directory?(file) || ::File.basename(file).start_with?(SKIPPED_FILE_PREFIX)
|
56
60
|
result << file
|
57
61
|
end
|
58
62
|
|
@@ -60,6 +64,7 @@ module Hanami
|
|
60
64
|
end
|
61
65
|
|
62
66
|
private
|
67
|
+
|
63
68
|
# @since 0.1.0
|
64
69
|
# @api private
|
65
70
|
def realpath(path)
|