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.
@@ -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)