hanami-assets 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 is fresh or changed from last check.
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.1.0
78
+ # @since 0.3.0
28
79
  # @api private
29
- def fresh?(file)
80
+ def modified?(file)
30
81
  @mutex.synchronize do
31
- @data[file.to_s] < mtime(file)
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] = mtime(file)
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: `#{ name }' (sources: #{ sources })")
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 `#{ ::File.basename(source) }'")
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 = 0644
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
- # @since 0.1.0
44
+ include Utils::ClassAttribute
45
+
46
+ # @since 0.3.0
41
47
  # @api private
42
- SASS_CACHE_LOCATION = Pathname(Hanami.respond_to?(:root) ?
43
- Hanami.root : Dir.pwd).join('tmp', 'sass-cache')
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
- new(configuration, name).compile
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 fresh?
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 ||= begin
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.1.0
173
+ # @since 0.3.0
145
174
  # @api private
146
- def fresh?
175
+ def modified?
147
176
  !destination.exist? ||
148
- cache.fresh?(source)
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
- !EXTENSIONS[::File.extname(source.to_s)]
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
- # NOTE `:load_paths' is useful only for Sass engine, to make `@include' directive to work.
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 x.x.x
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 sass_load_paths
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
- style: :compressed, cache_location: SASS_CACHE_LOCATION).render
33
+ style: :compressed, cache_location: SASS_CACHE_LOCATION).render
34
34
  end
35
35
  end
36
36
  end
@@ -27,7 +27,7 @@ module Hanami
27
27
 
28
28
  # @since 0.1.0
29
29
  # @api private
30
- alias_method :<<, :push
30
+ alias << push
31
31
 
32
32
  private
33
33
 
@@ -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: #{ path }")
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 `#{ asset }' in manifest (#{ manifest_path })")
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 relateive asset path
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
- Array.new.tap do |result|
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| "#{ source }#{ ::File::SEPARATOR }**#{ ::File::SEPARATOR }#{ name }*"}).each do |file|
55
- next if ::File.directory?(file) || ::File.basename(file).match(/\A\_/)
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)