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.
- 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
data/bin/hanami-assets
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: hanami-assets --config=path/to/config.rb"
|
9
|
+
|
10
|
+
opts.on("-c", "--config FILE", "Path to config") do |c|
|
11
|
+
options[:config] = c
|
12
|
+
end
|
13
|
+
end.parse!
|
14
|
+
|
15
|
+
config = options.fetch(:config) { raise ArgumentError.new("You must specify a configuration file") }
|
16
|
+
config = Pathname.new(config)
|
17
|
+
config.exist? or raise ArgumentError.new("Cannot find configuration file: #{ config }")
|
18
|
+
|
19
|
+
require 'hanami/assets'
|
20
|
+
load config
|
21
|
+
|
22
|
+
Hanami::Assets.deploy
|
data/hanami-assets.gemspec
CHANGED
@@ -4,20 +4,34 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'hanami/assets/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'hanami-assets'
|
8
8
|
spec.version = Hanami::Assets::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
|
10
|
+
spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
|
11
|
+
spec.summary = %q{Assets management}
|
12
|
+
spec.description = %q{Assets management for Ruby web applications}
|
13
|
+
spec.homepage = 'http://hanamirb.org'
|
14
|
+
spec.license = 'MIT'
|
11
15
|
|
12
|
-
spec.
|
13
|
-
spec.
|
14
|
-
spec.
|
16
|
+
spec.files = `git ls-files -- lib/* bin/* CHANGELOG.md LICENSE.md README.md hanami-assets.gemspec`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
spec.required_ruby_version = '>= 2.0.0'
|
15
21
|
|
16
|
-
spec.
|
17
|
-
spec.
|
18
|
-
spec.
|
19
|
-
spec.require_paths = ["lib"]
|
22
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 0.7'
|
23
|
+
spec.add_runtime_dependency 'hanami-helpers', '~> 0.3'
|
24
|
+
spec.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.2'
|
20
25
|
|
21
|
-
spec.add_development_dependency
|
22
|
-
spec.add_development_dependency
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'yui-compressor', '~> 0.12'
|
31
|
+
spec.add_development_dependency 'uglifier', '~> 2.7'
|
32
|
+
spec.add_development_dependency 'closure-compiler', '~> 1.1'
|
33
|
+
spec.add_development_dependency 'sass', '~> 3.4'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'coffee-script', '~> 2.3'
|
36
|
+
spec.add_development_dependency 'babel-transpiler', '~> 0.7'
|
23
37
|
end
|
data/lib/hanami/assets.rb
CHANGED
@@ -1,7 +1,158 @@
|
|
1
|
-
require
|
1
|
+
require 'thread'
|
2
|
+
require 'hanami/utils/class_attribute'
|
2
3
|
|
3
4
|
module Hanami
|
5
|
+
# Assets management for Ruby web applications
|
6
|
+
#
|
7
|
+
# @since 0.1.0
|
4
8
|
module Assets
|
5
|
-
#
|
9
|
+
# Base error for Hanami::Assets
|
10
|
+
#
|
11
|
+
# All the errors defined in this framework MUST inherit from it.
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
class Error < ::StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'hanami/assets/version'
|
18
|
+
require 'hanami/assets/configuration'
|
19
|
+
require 'hanami/assets/config/global_sources'
|
20
|
+
require 'hanami/assets/helpers'
|
21
|
+
|
22
|
+
include Utils::ClassAttribute
|
23
|
+
|
24
|
+
# Configuration
|
25
|
+
#
|
26
|
+
# @since 0.1.0
|
27
|
+
# @api private
|
28
|
+
class_attribute :configuration
|
29
|
+
self.configuration = Configuration.new
|
30
|
+
|
31
|
+
# Configure framework
|
32
|
+
#
|
33
|
+
# @param blk [Proc] configuration code block
|
34
|
+
#
|
35
|
+
# @return self
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
#
|
39
|
+
# @see Hanami::Assets::Configuration
|
40
|
+
def self.configure(&blk)
|
41
|
+
configuration.instance_eval(&blk)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Prepare assets for deploys
|
46
|
+
#
|
47
|
+
# @since 0.1.0
|
48
|
+
def self.deploy
|
49
|
+
require 'hanami/assets/precompiler'
|
50
|
+
require 'hanami/assets/bundler'
|
51
|
+
|
52
|
+
Precompiler.new(configuration, duplicates).run
|
53
|
+
Bundler.new(configuration, duplicates).run
|
54
|
+
end
|
55
|
+
|
56
|
+
# Preload the framework
|
57
|
+
#
|
58
|
+
# This MUST be used in production mode
|
59
|
+
#
|
60
|
+
# @since 0.1.0
|
61
|
+
#
|
62
|
+
# @example Direct Invocation
|
63
|
+
# require 'hanami/assets'
|
64
|
+
#
|
65
|
+
# Hanami::Assets.load!
|
66
|
+
#
|
67
|
+
# @example Load Via Configuration Block
|
68
|
+
# require 'hanami/assets'
|
69
|
+
#
|
70
|
+
# Hanami::Assets.configure do
|
71
|
+
# # ...
|
72
|
+
# end.load!
|
73
|
+
def self.load!
|
74
|
+
configuration.load!
|
75
|
+
end
|
76
|
+
|
77
|
+
# Global assets sources
|
78
|
+
#
|
79
|
+
# This is designed for third party integration gems with frontend frameworks
|
80
|
+
# like Bootstrap, Ember.js or React.
|
81
|
+
#
|
82
|
+
# Developers can maintain gems that ship static assets for these frameworks
|
83
|
+
# and make them available to Hanami::Assets.
|
84
|
+
#
|
85
|
+
# @return [Hanami::Assets::Config::GlobalSources]
|
86
|
+
#
|
87
|
+
# @since 0.1.0
|
88
|
+
#
|
89
|
+
# @example Ember.js Integration
|
90
|
+
# # lib/hanami/emberjs.rb (third party gem)
|
91
|
+
# require 'hanami/assets'
|
92
|
+
#
|
93
|
+
# Hanami::Assets.sources << '/path/to/emberjs/assets'
|
94
|
+
def self.sources
|
95
|
+
synchronize do
|
96
|
+
@@sources ||= Config::GlobalSources.new
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Duplicate the framework and generate modules for the target application
|
101
|
+
#
|
102
|
+
# @param mod [Module] the Ruby namespace of the application
|
103
|
+
# @param blk [Proc] an optional block to configure the framework
|
104
|
+
#
|
105
|
+
# @return [Module] a copy of Hanami::Assets
|
106
|
+
#
|
107
|
+
# @since 0.1.0
|
108
|
+
#
|
109
|
+
# @see Hanami::Assets#dupe
|
110
|
+
# @see Hanami::Assets::Configuration
|
111
|
+
def self.duplicate(mod, &blk)
|
112
|
+
dupe.tap do |duplicated|
|
113
|
+
duplicated.configure(&blk) if block_given?
|
114
|
+
duplicates << duplicated
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Duplicate Hanami::Assets in order to create a new separated instance
|
119
|
+
# of the framework.
|
120
|
+
#
|
121
|
+
# The new instance of the framework will be completely decoupled from the
|
122
|
+
# original. It will inherit the configuration, but all the changes that
|
123
|
+
# happen after the duplication, won't be reflected on the other copies.
|
124
|
+
#
|
125
|
+
# @return [Module] a copy of Hanami::Assets
|
126
|
+
#
|
127
|
+
# @since 0.1.0
|
128
|
+
# @api private
|
129
|
+
def self.dupe
|
130
|
+
dup.tap do |duplicated|
|
131
|
+
duplicated.configuration = configuration.duplicate
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Keep track of duplicated frameworks
|
136
|
+
#
|
137
|
+
# @return [Array] a collection of duplicated frameworks
|
138
|
+
#
|
139
|
+
# @since 0.1.0
|
140
|
+
# @api private
|
141
|
+
#
|
142
|
+
# @see Hanami::Assets#duplicate
|
143
|
+
# @see Hanami::Assets#dupe
|
144
|
+
def self.duplicates
|
145
|
+
synchronize do
|
146
|
+
@@duplicates ||= Array.new
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# @since 0.1.0
|
153
|
+
# @api private
|
154
|
+
def self.synchronize(&blk)
|
155
|
+
Mutex.new.synchronize(&blk)
|
156
|
+
end
|
6
157
|
end
|
7
158
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Assets
|
7
|
+
# Bundle assets from a single application.
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
# @api private
|
11
|
+
class Bundler
|
12
|
+
# @since 0.1.0
|
13
|
+
# @api private
|
14
|
+
DEFAULT_PERMISSIONS = 0644
|
15
|
+
|
16
|
+
# @since 0.1.0
|
17
|
+
# @api private
|
18
|
+
JAVASCRIPT_EXT = '.js'.freeze
|
19
|
+
|
20
|
+
# @since 0.1.0
|
21
|
+
# @api private
|
22
|
+
STYLESHEET_EXT = '.css'.freeze
|
23
|
+
|
24
|
+
# @since 0.1.0
|
25
|
+
# @api private
|
26
|
+
WILDCARD_EXT = '.*'.freeze
|
27
|
+
|
28
|
+
# @since 0.1.0
|
29
|
+
# @api private
|
30
|
+
URL_SEPARATOR = '/'.freeze
|
31
|
+
|
32
|
+
# @since 0.1.0
|
33
|
+
# @api private
|
34
|
+
URL_REPLACEMENT = ''.freeze
|
35
|
+
|
36
|
+
# Return a new instance
|
37
|
+
#
|
38
|
+
# @param configuration [Hanami::Assets::Configuration] a single application configuration
|
39
|
+
#
|
40
|
+
# @param duplicates [Array<Hanami::Assets>] the duplicated frameworks
|
41
|
+
# (one for each application)
|
42
|
+
#
|
43
|
+
# @return [Hanami::Assets::Bundler] a new instance
|
44
|
+
#
|
45
|
+
# @since 0.1.0
|
46
|
+
# @api private
|
47
|
+
def initialize(configuration, duplicates)
|
48
|
+
@manifest = Hash.new
|
49
|
+
@configuration = configuration
|
50
|
+
@configurations = if duplicates.empty?
|
51
|
+
[@configuration]
|
52
|
+
else
|
53
|
+
duplicates.map(&:configuration)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Start the process.
|
58
|
+
#
|
59
|
+
# For each asset contained in the sources and third party gems, it will:
|
60
|
+
#
|
61
|
+
# * Compress
|
62
|
+
# * Create a checksum version
|
63
|
+
#
|
64
|
+
# At the end it will generate a digest manifest
|
65
|
+
#
|
66
|
+
# @see Hanami::Assets::Configuration#digest
|
67
|
+
# @see Hanami::Assets::Configuration#manifest
|
68
|
+
# @see Hanami::Assets::Configuration#manifest_path
|
69
|
+
def run
|
70
|
+
assets.each do |asset|
|
71
|
+
next if ::File.directory?(asset)
|
72
|
+
|
73
|
+
compress(asset)
|
74
|
+
checksum(asset)
|
75
|
+
end
|
76
|
+
|
77
|
+
generate_manifest
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# @since 0.1.0
|
83
|
+
# @api private
|
84
|
+
def assets
|
85
|
+
Dir.glob("#{ public_directory }#{ ::File::SEPARATOR }**#{ ::File::SEPARATOR }*")
|
86
|
+
end
|
87
|
+
|
88
|
+
# @since 0.1.0
|
89
|
+
# @api private
|
90
|
+
def compress(asset)
|
91
|
+
case File.extname(asset)
|
92
|
+
when JAVASCRIPT_EXT then _compress(compressor(:js, asset), asset)
|
93
|
+
when STYLESHEET_EXT then _compress(compressor(:css, asset), asset)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# @since 0.1.0
|
98
|
+
# @api private
|
99
|
+
def checksum(asset)
|
100
|
+
digest = Digest::MD5.file(asset)
|
101
|
+
filename, ext = ::File.basename(asset, WILDCARD_EXT), ::File.extname(asset)
|
102
|
+
directory = ::File.dirname(asset)
|
103
|
+
target = [directory, "#{ filename }-#{ digest }#{ ext }"].join(::File::SEPARATOR)
|
104
|
+
|
105
|
+
FileUtils.cp(asset, target)
|
106
|
+
_set_permissions(target)
|
107
|
+
|
108
|
+
store_manifest(asset, target)
|
109
|
+
end
|
110
|
+
|
111
|
+
# @since 0.1.0
|
112
|
+
# @api private
|
113
|
+
def generate_manifest
|
114
|
+
_write(@configuration.manifest_path, JSON.dump(@manifest))
|
115
|
+
end
|
116
|
+
|
117
|
+
# @since 0.1.0
|
118
|
+
# @api private
|
119
|
+
def store_manifest(asset, target)
|
120
|
+
@manifest[_convert_to_url(::File.expand_path(asset))] = _convert_to_url(::File.expand_path(target))
|
121
|
+
end
|
122
|
+
|
123
|
+
# @since 0.1.0
|
124
|
+
# @api private
|
125
|
+
def compressor(type, asset)
|
126
|
+
_configuration_for(asset).__send__(:"#{ type }_compressor")
|
127
|
+
end
|
128
|
+
|
129
|
+
# @since 0.1.0
|
130
|
+
# @api private
|
131
|
+
def _compress(compressor, asset)
|
132
|
+
_write(asset, compressor.compress(asset))
|
133
|
+
rescue => e
|
134
|
+
warn "Skipping compression of: `#{ asset }'\nReason: #{ e }\n\t#{ e.backtrace.join("\n\t") }\n\n"
|
135
|
+
end
|
136
|
+
|
137
|
+
# @since 0.1.0
|
138
|
+
# @api private
|
139
|
+
def _convert_to_url(path)
|
140
|
+
path.sub(public_directory.to_s, URL_REPLACEMENT).
|
141
|
+
gsub(File::SEPARATOR, URL_SEPARATOR)
|
142
|
+
end
|
143
|
+
|
144
|
+
# @since 0.1.0
|
145
|
+
# @api private
|
146
|
+
def _write(path, content)
|
147
|
+
Pathname.new(path).dirname.mkpath
|
148
|
+
::File.write(path, content)
|
149
|
+
|
150
|
+
_set_permissions(path)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @since 0.1.0
|
154
|
+
# @api private
|
155
|
+
def _set_permissions(path)
|
156
|
+
::File.chmod(DEFAULT_PERMISSIONS, path)
|
157
|
+
end
|
158
|
+
|
159
|
+
def _configuration_for(asset)
|
160
|
+
url = _convert_to_url(asset)
|
161
|
+
|
162
|
+
@configurations.find {|config| url.start_with?(config.prefix) } ||
|
163
|
+
@configuration
|
164
|
+
end
|
165
|
+
|
166
|
+
# @since 0.1.0
|
167
|
+
# @api private
|
168
|
+
def public_directory
|
169
|
+
@configuration.public_directory
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Assets
|
5
|
+
# Store assets references when compile mode is on.
|
6
|
+
#
|
7
|
+
# This is expecially useful in development mode, where we want to compile
|
8
|
+
# only the assets that were changed from last browser refresh.
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
# @api private
|
12
|
+
class Cache
|
13
|
+
# Return a new instance
|
14
|
+
#
|
15
|
+
# @return [Hanami::Assets::Cache] a new instance
|
16
|
+
def initialize
|
17
|
+
@data = Hash.new{|h,k| h[k] = 0 }
|
18
|
+
@mutex = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if the given file is fresh or changed from last check.
|
22
|
+
#
|
23
|
+
# @param file [String,Pathname] the file path
|
24
|
+
#
|
25
|
+
# @return [TrueClass,FalseClass] the result of the check
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
# @api private
|
29
|
+
def fresh?(file)
|
30
|
+
@mutex.synchronize do
|
31
|
+
@data[file.to_s] < mtime(file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Store the given file reference
|
36
|
+
#
|
37
|
+
# @param file [String,Pathname] the file path
|
38
|
+
#
|
39
|
+
# @return [TrueClass,FalseClass] the result of the check
|
40
|
+
#
|
41
|
+
# @since 0.1.0
|
42
|
+
# @api private
|
43
|
+
def store(file)
|
44
|
+
@mutex.synchronize do
|
45
|
+
@data[file.to_s] = mtime(file)
|
46
|
+
end
|
47
|
+
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
|
+
end
|
57
|
+
end
|
58
|
+
end
|