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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/LICENSE.md +22 -0
- data/README.md +426 -9
- data/bin/hanami-assets +22 -0
- data/hanami-assets.gemspec +26 -12
- data/lib/hanami/assets.rb +153 -2
- data/lib/hanami/assets/bundler.rb +173 -0
- data/lib/hanami/assets/cache.rb +58 -0
- data/lib/hanami/assets/compiler.rb +212 -0
- data/lib/hanami/assets/compressors/abstract.rb +119 -0
- data/lib/hanami/assets/compressors/builtin_javascript.rb +36 -0
- data/lib/hanami/assets/compressors/builtin_stylesheet.rb +57 -0
- data/lib/hanami/assets/compressors/closure_javascript.rb +25 -0
- data/lib/hanami/assets/compressors/javascript.rb +77 -0
- data/lib/hanami/assets/compressors/jsmin.rb +283 -0
- data/lib/hanami/assets/compressors/null_compressor.rb +19 -0
- data/lib/hanami/assets/compressors/sass_stylesheet.rb +38 -0
- data/lib/hanami/assets/compressors/stylesheet.rb +77 -0
- data/lib/hanami/assets/compressors/uglifier_javascript.rb +25 -0
- data/lib/hanami/assets/compressors/yui_javascript.rb +25 -0
- data/lib/hanami/assets/compressors/yui_stylesheet.rb +25 -0
- data/lib/hanami/assets/config/global_sources.rb +50 -0
- data/lib/hanami/assets/config/manifest.rb +112 -0
- data/lib/hanami/assets/config/sources.rb +77 -0
- data/lib/hanami/assets/configuration.rb +539 -0
- data/lib/hanami/assets/helpers.rb +733 -0
- data/lib/hanami/assets/precompiler.rb +67 -0
- data/lib/hanami/assets/version.rb +4 -1
- metadata +189 -17
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|