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
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
@@ -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 = "hanami-assets"
7
+ spec.name = 'hanami-assets'
8
8
  spec.version = Hanami::Assets::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
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.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
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.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
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 "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
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 "hanami/assets/version"
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
- # Your code goes here...
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