hanami-assets 0.0.0 → 0.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +426 -9
  5. data/bin/hanami-assets +22 -0
  6. data/hanami-assets.gemspec +26 -12
  7. data/lib/hanami/assets.rb +153 -2
  8. data/lib/hanami/assets/bundler.rb +173 -0
  9. data/lib/hanami/assets/cache.rb +58 -0
  10. data/lib/hanami/assets/compiler.rb +212 -0
  11. data/lib/hanami/assets/compressors/abstract.rb +119 -0
  12. data/lib/hanami/assets/compressors/builtin_javascript.rb +36 -0
  13. data/lib/hanami/assets/compressors/builtin_stylesheet.rb +57 -0
  14. data/lib/hanami/assets/compressors/closure_javascript.rb +25 -0
  15. data/lib/hanami/assets/compressors/javascript.rb +77 -0
  16. data/lib/hanami/assets/compressors/jsmin.rb +283 -0
  17. data/lib/hanami/assets/compressors/null_compressor.rb +19 -0
  18. data/lib/hanami/assets/compressors/sass_stylesheet.rb +38 -0
  19. data/lib/hanami/assets/compressors/stylesheet.rb +77 -0
  20. data/lib/hanami/assets/compressors/uglifier_javascript.rb +25 -0
  21. data/lib/hanami/assets/compressors/yui_javascript.rb +25 -0
  22. data/lib/hanami/assets/compressors/yui_stylesheet.rb +25 -0
  23. data/lib/hanami/assets/config/global_sources.rb +50 -0
  24. data/lib/hanami/assets/config/manifest.rb +112 -0
  25. data/lib/hanami/assets/config/sources.rb +77 -0
  26. data/lib/hanami/assets/configuration.rb +539 -0
  27. data/lib/hanami/assets/helpers.rb +733 -0
  28. data/lib/hanami/assets/precompiler.rb +67 -0
  29. data/lib/hanami/assets/version.rb +4 -1
  30. metadata +189 -17
  31. data/.gitignore +0 -9
  32. data/Gemfile +0 -4
  33. data/Rakefile +0 -2
  34. data/bin/console +0 -14
  35. data/bin/setup +0 -8
@@ -0,0 +1,212 @@
1
+ module Hanami
2
+ module Assets
3
+ class MissingAsset < Error
4
+ def initialize(name, sources)
5
+ sources = sources.map(&:to_s).join(', ')
6
+ super("Missing asset: `#{ name }' (sources: #{ sources })")
7
+ end
8
+ end
9
+
10
+ class UnknownAssetEngine < Error
11
+ def initialize(source)
12
+ super("No asset engine registered for `#{ ::File.basename(source) }'")
13
+ end
14
+ end
15
+
16
+ # Assets compiler
17
+ #
18
+ # It compiles assets that needs to be preprocessed (eg. Sass or ES6) into
19
+ # the destination directory.
20
+ #
21
+ # Vanilla javascripts or stylesheets are just copied over.
22
+ #
23
+ # @since 0.1.0
24
+ # @api private
25
+ class Compiler
26
+ # @since 0.1.0
27
+ # @api private
28
+ DEFAULT_PERMISSIONS = 0644
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ COMPILE_PATTERN = '*.*.*'.freeze # Example hello.js.es6
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ EXTENSIONS = {'.js' => true, '.css' => true}.freeze
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ SASS_CACHE_LOCATION = Pathname(Hanami.respond_to?(:root) ?
41
+ Hanami.root : Dir.pwd).join('tmp', 'sass-cache')
42
+
43
+ # Compile the given asset
44
+ #
45
+ # @param configuration [Hanami::Assets::Configuration] the application
46
+ # configuration associated with the given asset
47
+ #
48
+ # @param name [String] the asset path
49
+ #
50
+ # @since 0.1.0
51
+ # @api private
52
+ def self.compile(configuration, name)
53
+ return unless configuration.compile
54
+
55
+ require 'tilt'
56
+ require 'hanami/assets/cache'
57
+ new(configuration, name).compile
58
+ end
59
+
60
+ # Assets cache
61
+ #
62
+ # @since 0.1.0
63
+ # @api private
64
+ #
65
+ # @see Hanami::Assets::Cache
66
+ def self.cache
67
+ @@cache ||= Assets::Cache.new
68
+ end
69
+
70
+ # Return a new instance
71
+ #
72
+ # @param configuration [Hanami::Assets::Configuration] the application
73
+ # configuration associated with the given asset
74
+ #
75
+ # @param name [String] the asset path
76
+ #
77
+ # @return [Hanami::Assets::Compiler] a new instance
78
+ #
79
+ # @since 0.1.0
80
+ # @api private
81
+ def initialize(configuration, name)
82
+ @configuration = configuration
83
+ @name = Pathname.new(name)
84
+ end
85
+
86
+ # Compile the asset
87
+ #
88
+ # @raise [Hanami::Assets::MissingAsset] if the asset can't be found in
89
+ # sources
90
+ #
91
+ # @since 0.1.0
92
+ # @api private
93
+ def compile
94
+ raise MissingAsset.new(@name, @configuration.sources) unless exist?
95
+ return unless fresh?
96
+
97
+ if compile?
98
+ compile!
99
+ else
100
+ copy!
101
+ end
102
+
103
+ cache!
104
+ end
105
+
106
+ private
107
+
108
+ # @since 0.1.0
109
+ # @api private
110
+ def source
111
+ @source ||= begin
112
+ @name.absolute? ? @name :
113
+ @configuration.find(@name)
114
+ end
115
+ end
116
+
117
+ # @since 0.1.0
118
+ # @api private
119
+ def destination
120
+ @destination ||= @configuration.destination_directory.join(basename)
121
+ end
122
+
123
+ # @since 0.1.0
124
+ # @api private
125
+ def basename
126
+ result = ::File.basename(@name)
127
+
128
+ if compile?
129
+ result.scan(/\A[[[:alnum:]][\-\_]]*\.[[\w]]*/).first || result
130
+ else
131
+ result
132
+ end
133
+ end
134
+
135
+ # @since 0.1.0
136
+ # @api private
137
+ def exist?
138
+ !source.nil? &&
139
+ source.exist?
140
+ end
141
+
142
+ # @since 0.1.0
143
+ # @api private
144
+ def fresh?
145
+ !destination.exist? ||
146
+ cache.fresh?(source)
147
+ end
148
+
149
+ # @since 0.1.0
150
+ # @api private
151
+ def compile?
152
+ @compile ||= ::File.fnmatch(COMPILE_PATTERN, source.to_s) &&
153
+ !EXTENSIONS[::File.extname(source.to_s)]
154
+ end
155
+
156
+ # @since 0.1.0
157
+ # @api private
158
+ def compile!
159
+ # NOTE `:load_paths' is useful only for Sass engine, to make `@include' directive to work.
160
+ # For now we don't want to maintan a specialized Compiler version for Sass.
161
+ #
162
+ # If in the future other precompilers will need special treatment,
163
+ # we can consider to maintain several specialized versions in order to
164
+ # don't add a perf tax to all the other preprocessors who "just work".
165
+ #
166
+ # Example: if Less "just works", we can keep it in the general `Compiler',
167
+ # but have a `SassCompiler` if it requires more than `:load_paths'.
168
+ #
169
+ # NOTE: We need another option to pass for Sass: `:cache_location'.
170
+ #
171
+ # This is needed to don't create a `.sass-cache' directory at the root of the project,
172
+ # but to have it under `tmp/sass-cache'.
173
+ write { Tilt.new(source, nil, load_paths: @configuration.sources.to_a, cache_location: sass_cache_location).render }
174
+ rescue RuntimeError
175
+ raise UnknownAssetEngine.new(source)
176
+ end
177
+
178
+ # @since 0.1.0
179
+ # @api private
180
+ def copy!
181
+ write { source.read }
182
+ end
183
+
184
+ # @since 0.1.0
185
+ # @api private
186
+ def cache!
187
+ cache.store(source)
188
+ end
189
+
190
+ # @since 0.1.0
191
+ # @api private
192
+ def write
193
+ destination.dirname.mkpath
194
+ destination.open(File::WRONLY|File::CREAT, DEFAULT_PERMISSIONS) do |file|
195
+ file.write(yield)
196
+ end
197
+ end
198
+
199
+ # @since 0.1.0
200
+ # @api private
201
+ def cache
202
+ self.class.cache
203
+ end
204
+
205
+ # @since 0.1.0
206
+ # @api private
207
+ def sass_cache_location
208
+ SASS_CACHE_LOCATION
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,119 @@
1
+ require 'hanami/utils/string'
2
+ require 'hanami/utils/class'
3
+
4
+ module Hanami
5
+ module Assets
6
+ module Compressors
7
+ # Unknown compressor error
8
+ #
9
+ # It's raised when trying to load an unknown compressor.
10
+ #
11
+ # @since 0.1.0
12
+ # @api private
13
+ #
14
+ # @see Hanami::Assets::Configuration#javascript_compressor
15
+ # @see Hanami::Assets::Configuration#stylesheet_compressor
16
+ # @see Hanami::Assets::Compressors::Abstract#for
17
+ class UnknownCompressorError < Error
18
+ # @since 0.1.0
19
+ # @api private
20
+ def initialize(type, engine_name)
21
+ super("Unknown #{ type } compressor: :#{ engine_name }")
22
+ end
23
+ end
24
+
25
+ # Abstract base class for compressors.
26
+ #
27
+ # Don't use this class directly, but please use subclasses instead.
28
+ #
29
+ # @since 0.1.0
30
+ # @api private
31
+ class Abstract
32
+ # Compress the given asset
33
+ #
34
+ # @param filename [String, Pathname] the absolute path to the asset
35
+ #
36
+ # @return [String] the compressed asset
37
+ #
38
+ # @since 0.1.0
39
+ # @api private
40
+ def compress(filename)
41
+ compressor.compress(
42
+ read(filename)
43
+ )
44
+ end
45
+
46
+ protected
47
+ # @since 0.1.0
48
+ # @api private
49
+ attr_reader :compressor
50
+
51
+ # Read the contents of given filename
52
+ #
53
+ # @param filename [String, Pathname] the absolute path to the asset
54
+ #
55
+ # @return [String] the contents of asset
56
+ #
57
+ # @since 0.1.0
58
+ # @api private
59
+ def read(filename)
60
+ ::File.read(filename)
61
+ end
62
+
63
+ private
64
+
65
+ # Factory for compressors.
66
+ #
67
+ # It loads a compressor for the given name.
68
+ #
69
+ # @abstract Please use this method from the subclasses
70
+ #
71
+ # @param engine_name [Symbol,String,NilClass,#compress] the name of the
72
+ # engine to load or an instance of an engine
73
+ #
74
+ # @return [Hanami::Assets::Compressors::Abstract] returns a concrete
75
+ # implementation of a compressor
76
+ #
77
+ # @raise [Hanami::Assets::Compressors::UnknownCompressorError] when the
78
+ # given name refers to an unknown compressor engine
79
+ #
80
+ # @since 0.1.0
81
+ # @api private
82
+ def self.for(engine_name)
83
+ case engine_name
84
+ when Symbol, String
85
+ load_engine(name, engine_name)
86
+ when nil
87
+ require 'hanami/assets/compressors/null_compressor'
88
+ NullCompressor.new
89
+ else
90
+ engine_name
91
+ end
92
+ end
93
+
94
+ # Load the compressor for the given type and engine name.
95
+ #
96
+ # @param type [String] asset type (eg. "Javascript" or "Stylesheet")
97
+ # @param engine_name [Symbol,String] the name of the engine to load (eg. `:yui`)
98
+ #
99
+ # @return [Hanami::Assets::Compress::Abstract] returns a concrete
100
+ # implementation of a compressor
101
+ #
102
+ # @since 0.1.0
103
+ # @api private
104
+ def self.load_engine(type, engine_name)
105
+ type = Utils::String.new(type).demodulize
106
+
107
+ require "hanami/assets/compressors/#{ engine_name }_#{ type.underscore }"
108
+ Utils::Class.load!("#{ Utils::String.new(engine_name).classify }#{ type }", Hanami::Assets::Compressors).new
109
+ rescue LoadError
110
+ raise UnknownCompressorError.new(type, engine_name)
111
+ end
112
+
113
+ class << self
114
+ private :for, :load_engine
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,36 @@
1
+ require 'hanami/assets/compressors/javascript'
2
+ require_relative './jsmin'
3
+
4
+ module Hanami
5
+ module Assets
6
+ module Compressors
7
+ # Builtin compressor for stylesheet
8
+ #
9
+ # This is a port of jsmin
10
+ # Copyright (c) 2002 Douglas Crockford (www.crockford.com)
11
+ #
12
+ # This Ruby port was implemented by Ryan Grove (@rgrove) as work for
13
+ # <tt>jsmin</tt> gem.
14
+ #
15
+ # Copyright (c) 2008-2012 Ryan Grove
16
+ #
17
+ # @since 0.1.0
18
+ # @api private
19
+ #
20
+ # @see https://github.com/sbecker/asset_packager
21
+ class BuiltinJavascript < Javascript
22
+ def initialize
23
+ @compressor = JSMin
24
+ end
25
+
26
+ # @since 0.1.0
27
+ # @api private
28
+ def compress(filename)
29
+ compressor.minify(
30
+ read(filename)
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ require 'hanami/assets/compressors/stylesheet'
2
+
3
+ module Hanami
4
+ module Assets
5
+ module Compressors
6
+ # Builtin compressor for stylesheet
7
+ #
8
+ # This is a basic algorithm based on Scott Becker (@sbecker) work on
9
+ # <tt>asset_packager</tt> gem.
10
+ #
11
+ # Copyright (c) 2006-2008 Scott Becker
12
+ #
13
+ # @since 0.1.0
14
+ # @api private
15
+ #
16
+ # @see https://github.com/sbecker/asset_packager
17
+ class BuiltinStylesheet < Stylesheet
18
+ # @since 0.1.0
19
+ # @api private
20
+ SPACE_REPLACEMENT = " ".freeze
21
+
22
+ # @since 0.1.0
23
+ # @api private
24
+ COMMENTS_REPLACEMENT = "".freeze
25
+
26
+ # @since 0.1.0
27
+ # @api private
28
+ LINE_BREAKS_REPLACEMENT = "}\n".freeze
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ LAST_BREAK_REPLACEMENT = "".freeze
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ INSIDE_LEFT_BRACKET_REPLACEMENT = " {".freeze
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ INSIDE_RIGHT_BRACKET_REPLACEMENT = "}".freeze
41
+
42
+ # @since 0.1.0
43
+ # @api private
44
+ def compress(filename)
45
+ result = read(filename)
46
+ result.gsub!(/\s+/, SPACE_REPLACEMENT) # collapse space
47
+ result.gsub!(/\/\*(.*?)\*\/ /, COMMENTS_REPLACEMENT) # remove comments - caution, might want to remove this if using css hacks
48
+ result.gsub!(/\} /, LINE_BREAKS_REPLACEMENT) # add line breaks
49
+ result.gsub!(/\n$/, LAST_BREAK_REPLACEMENT) # remove last break
50
+ result.gsub!(/ \{ /, INSIDE_LEFT_BRACKET_REPLACEMENT) # trim inside brackets
51
+ result.gsub!(/; \}/, INSIDE_RIGHT_BRACKET_REPLACEMENT) # trim inside brackets
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ require 'hanami/assets/compressors/javascript'
2
+ require 'closure-compiler'
3
+
4
+ module Hanami
5
+ module Assets
6
+ module Compressors
7
+ # Google Closure Compiler for JavaScript
8
+ #
9
+ # Depends on <tt>closure-compiler</tt> gem
10
+ #
11
+ # @see https://developers.google.com/closure/compiler
12
+ # @see https://rubygems.org/gems/closure-compiler
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ class ClosureJavascript < Javascript
17
+ # @since 0.1.0
18
+ # @api private
19
+ def initialize
20
+ @compressor = Closure::Compiler.new
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end