hanami-assets 0.0.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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