bunch 0.2.2 → 1.0.0pre1
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 +7 -0
- data/.gitignore +15 -6
- data/Gemfile +3 -1
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +7 -12
- data/bin/bunch +2 -5
- data/bunch.gemspec +30 -23
- data/lib/bunch.rb +37 -81
- data/lib/bunch/cli.rb +40 -74
- data/lib/bunch/combiner.rb +121 -0
- data/lib/bunch/compiler.rb +52 -0
- data/lib/bunch/compilers/coffee_script.rb +23 -0
- data/lib/bunch/compilers/ejs.rb +28 -0
- data/lib/bunch/compilers/jade.rb +28 -0
- data/lib/bunch/compilers/jst.rb +38 -0
- data/lib/bunch/compilers/null.rb +19 -0
- data/lib/bunch/compilers/sass.rb +55 -0
- data/lib/bunch/content_hash.rb +37 -0
- data/lib/bunch/css_minifier.rb +121 -0
- data/lib/bunch/file.rb +18 -0
- data/lib/bunch/file_cache.rb +159 -0
- data/lib/bunch/file_tree.rb +153 -0
- data/lib/bunch/ignorer.rb +38 -0
- data/lib/bunch/js_minifier.rb +38 -0
- data/lib/bunch/middleware.rb +16 -67
- data/lib/bunch/pipeline.rb +30 -0
- data/lib/bunch/server.rb +56 -0
- data/lib/bunch/simple_cache.rb +36 -0
- data/lib/bunch/tree_merge.rb +29 -0
- data/lib/bunch/version.rb +3 -1
- data/spec/bunch/cli_spec.rb +85 -0
- data/spec/bunch/combiner_spec.rb +107 -0
- data/spec/bunch/compiler_spec.rb +73 -0
- data/spec/bunch/compilers/coffee_script_spec.rb +23 -0
- data/spec/bunch/compilers/ejs_spec.rb +27 -0
- data/spec/bunch/compilers/jade_spec.rb +28 -0
- data/spec/bunch/compilers/sass_spec.rb +120 -0
- data/spec/bunch/css_minifier_spec.rb +31 -0
- data/spec/bunch/file_cache_spec.rb +151 -0
- data/spec/bunch/file_tree_spec.rb +127 -0
- data/spec/bunch/ignorer_spec.rb +26 -0
- data/spec/bunch/js_minifier_spec.rb +35 -0
- data/spec/bunch/middleware_spec.rb +41 -0
- data/spec/bunch/pipeline_spec.rb +31 -0
- data/spec/bunch/server_spec.rb +90 -0
- data/spec/bunch/simple_cache_spec.rb +55 -0
- data/spec/bunch/tree_merge_spec.rb +30 -0
- data/spec/bunch_spec.rb +6 -0
- data/spec/example_tree/directory/_combine +2 -0
- data/{example/js/test1.js → spec/example_tree/directory/file1} +0 -0
- data/{example/js/test2/test2a.js → spec/example_tree/directory/file2} +0 -0
- data/{example/js/test2/test2c.js → spec/example_tree/file3} +0 -0
- data/spec/spec_helper.rb +38 -0
- metadata +224 -102
- data/.yardopts +0 -1
- data/README.md +0 -4
- data/config.ru +0 -6
- data/example/config.ru +0 -6
- data/example/css/test1.css +0 -1
- data/example/css/test2/test2a.scss +0 -1
- data/example/css/test2/test2b.css +0 -1
- data/example/js/.bunchignore +0 -1
- data/example/js/test2/_.yml +0 -2
- data/example/js/test2/foo.js +0 -1
- data/example/js/test2/test2b.js +0 -1
- data/example/js/test3/test3a.js +0 -1
- data/example/js/test3/test3b/_.yml +0 -1
- data/example/js/test3/test3b/test3bi.js +0 -1
- data/example/js/test3/test3b/test3bii.js +0 -1
- data/example/js/test4/_.yml +0 -1
- data/example/js/test4/test4a.js +0 -1
- data/example/js/test4/test4b.coffee +0 -1
- data/example/js/test4/test4c.coffee +0 -1
- data/example/js/test5/test5a.jst.ejs +0 -1
- data/lib/bunch/abstract_node.rb +0 -25
- data/lib/bunch/cache.rb +0 -40
- data/lib/bunch/coffee_node.rb +0 -39
- data/lib/bunch/directory_node.rb +0 -82
- data/lib/bunch/ejs_node.rb +0 -50
- data/lib/bunch/file_node.rb +0 -25
- data/lib/bunch/jade_node.rb +0 -50
- data/lib/bunch/null_node.rb +0 -11
- data/lib/bunch/rack.rb +0 -38
- data/lib/bunch/sass_node.rb +0 -39
- data/test/middleware_test.rb +0 -26
- data/test/rack_test.rb +0 -93
- data/test/test_helper.rb +0 -21
data/lib/bunch/file.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
class File
|
5
|
+
attr_reader :path, :content, :filename, :extension
|
6
|
+
|
7
|
+
def initialize(path, content)
|
8
|
+
@path = path
|
9
|
+
@content = content
|
10
|
+
@filename = ::File.basename(@path)
|
11
|
+
@extension = ::File.extname(@filename)
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept(visitor)
|
15
|
+
visitor.visit_file(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "digest/md5"
|
4
|
+
require "fileutils"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
module Bunch
|
8
|
+
def self.FileCache(*args)
|
9
|
+
FileCache.new(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
class FileCache
|
13
|
+
Result = Struct.new(:result)
|
14
|
+
|
15
|
+
def initialize(processor_class, input_dir, *args)
|
16
|
+
@processor_class, @args = processor_class, args
|
17
|
+
@cache_name = "#{processor_class}-#{input_dir}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def new(tree)
|
21
|
+
cache = load_cache
|
22
|
+
partition = Partition.new(tree, cache)
|
23
|
+
|
24
|
+
partition.process!
|
25
|
+
|
26
|
+
processor = @processor_class.new(partition.pending, *@args)
|
27
|
+
result = TreeMerge.new(partition.cached, processor.result).result
|
28
|
+
|
29
|
+
save_cache tree, result
|
30
|
+
Result.new(result)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_cache
|
36
|
+
FileUtils.mkdir_p cache_directory
|
37
|
+
Cache.read_from_file cache_path
|
38
|
+
end
|
39
|
+
|
40
|
+
def save_cache(input, output)
|
41
|
+
FileUtils.mkdir_p cache_directory
|
42
|
+
Cache.from_trees(input, output).write_to_file cache_path
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: Make configurable
|
46
|
+
def cache_directory
|
47
|
+
".bunch-cache"
|
48
|
+
end
|
49
|
+
|
50
|
+
def cache_filename
|
51
|
+
@cache_name.gsub(/[:\/]/, "-")
|
52
|
+
end
|
53
|
+
|
54
|
+
def cache_path
|
55
|
+
::File.join(cache_directory, cache_filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
class Partition
|
59
|
+
attr_reader :pending, :cached
|
60
|
+
|
61
|
+
def initialize(input, cache)
|
62
|
+
@input = input
|
63
|
+
@cache = cache
|
64
|
+
end
|
65
|
+
|
66
|
+
def process!
|
67
|
+
@path = []
|
68
|
+
@pending = FileTree.new
|
69
|
+
@cached = FileTree.new
|
70
|
+
@input.accept(self)
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def enter_tree(tree)
|
75
|
+
@path << tree.name if tree.name
|
76
|
+
end
|
77
|
+
|
78
|
+
def leave_tree(tree)
|
79
|
+
@path.pop if tree.name
|
80
|
+
end
|
81
|
+
|
82
|
+
def visit_file(file)
|
83
|
+
file_path = [*@path, file.path].join("/")
|
84
|
+
cached_content = @cache.read(file_path, file.content)
|
85
|
+
if cached_content
|
86
|
+
@cached.write file_path, cached_content
|
87
|
+
else
|
88
|
+
@pending.write file_path, file.content
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Cache
|
94
|
+
def self.from_trees(input, output)
|
95
|
+
new(HashHasher.new(input).result, output)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.read_from_file(path)
|
99
|
+
version, hashes, output = YAML.load_file(path)
|
100
|
+
if version == VERSION
|
101
|
+
new(hashes, FileTree.from_hash(output))
|
102
|
+
else
|
103
|
+
empty
|
104
|
+
end
|
105
|
+
rescue Errno::ENOENT
|
106
|
+
empty
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.empty
|
110
|
+
new({}, FileTree.new)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.digest(contents)
|
114
|
+
Digest::MD5.hexdigest(contents)
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize(hashes, output)
|
118
|
+
@hashes, @output = hashes, output
|
119
|
+
end
|
120
|
+
|
121
|
+
def read(path, input_contents)
|
122
|
+
if Cache.digest(input_contents) == @hashes[path]
|
123
|
+
@output.get(path).content
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_to_file(path)
|
128
|
+
yaml = YAML.dump([Bunch::VERSION, @hashes, @output.to_hash])
|
129
|
+
::File.open(path, "w") { |f| f.write yaml }
|
130
|
+
end
|
131
|
+
|
132
|
+
class HashHasher
|
133
|
+
def initialize(tree)
|
134
|
+
@input = tree
|
135
|
+
end
|
136
|
+
|
137
|
+
def result
|
138
|
+
@path = []
|
139
|
+
@hashes = {}
|
140
|
+
@input.accept(self)
|
141
|
+
@hashes
|
142
|
+
end
|
143
|
+
|
144
|
+
def enter_tree(tree)
|
145
|
+
@path << tree.name if tree.name
|
146
|
+
end
|
147
|
+
|
148
|
+
def leave_tree(tree)
|
149
|
+
@path.pop if tree.name
|
150
|
+
end
|
151
|
+
|
152
|
+
def visit_file(file)
|
153
|
+
file_path = [*@path, file.path].join("/")
|
154
|
+
@hashes[file_path] = Digest::MD5.hexdigest(file.content)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
class FileTree
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :path, :name
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def from_hash(hash)
|
13
|
+
new nil, hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def from_path(path)
|
17
|
+
new nil, hash_from_path(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def hash_from_path(path)
|
23
|
+
hash = {}
|
24
|
+
Dir.foreach(path) do |entry|
|
25
|
+
next if entry == "." || entry == ".."
|
26
|
+
this_path = "#{path}/#{entry}"
|
27
|
+
if ::File.directory? this_path
|
28
|
+
hash[entry] = hash_from_path this_path
|
29
|
+
else
|
30
|
+
hash[entry] = ::File.read this_path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
hash
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(path = nil, hash = {})
|
38
|
+
@path = path
|
39
|
+
@hash = hash
|
40
|
+
@name = ::File.basename(@path) if @path
|
41
|
+
end
|
42
|
+
|
43
|
+
def directory?(path)
|
44
|
+
look_up_path(path).is_a?(Hash)
|
45
|
+
end
|
46
|
+
|
47
|
+
def exist?(filename)
|
48
|
+
!!look_up_path(filename)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get(path)
|
52
|
+
case (content = look_up_path(path))
|
53
|
+
when Hash
|
54
|
+
FileTree.new(path, content)
|
55
|
+
when String
|
56
|
+
File.new(path, content)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Like get, but takes a path with no extension and returns the first match
|
61
|
+
# regardless of its extension.
|
62
|
+
def get_fuzzy(fuzzy_path)
|
63
|
+
path = fuzzy_find_path(fuzzy_path)
|
64
|
+
get(path) if path
|
65
|
+
end
|
66
|
+
|
67
|
+
def write(path, contents)
|
68
|
+
dirname, _, filename = path.rpartition("/")
|
69
|
+
|
70
|
+
if dirname == ""
|
71
|
+
@hash[filename] = contents
|
72
|
+
else
|
73
|
+
unless exist?(dirname)
|
74
|
+
write(dirname, {})
|
75
|
+
end
|
76
|
+
|
77
|
+
unless directory?(dirname)
|
78
|
+
raise "#{dirname.inspect} is a file, not a directory!"
|
79
|
+
end
|
80
|
+
|
81
|
+
get(dirname).write(filename, contents)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def each
|
86
|
+
@hash.keys.each do |filename|
|
87
|
+
yield [filename, get(filename)]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_hash
|
92
|
+
@hash
|
93
|
+
end
|
94
|
+
|
95
|
+
def accept(visitor)
|
96
|
+
unless visitor.enter_tree(self) == false
|
97
|
+
each { |_, node| break if node.accept(visitor) == false }
|
98
|
+
end
|
99
|
+
visitor.leave_tree(self)
|
100
|
+
end
|
101
|
+
|
102
|
+
def write_to_path(path)
|
103
|
+
write_hash_to_path path, @hash
|
104
|
+
end
|
105
|
+
|
106
|
+
def ==(other)
|
107
|
+
to_hash == other.to_hash
|
108
|
+
end
|
109
|
+
|
110
|
+
def dup
|
111
|
+
FileTree.new(@path, Marshal.load(Marshal.dump(@hash)))
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def look_up_path(filename)
|
117
|
+
filename.split("/").inject(@hash) do |hash, path_component|
|
118
|
+
break if hash.nil? || hash.is_a?(String)
|
119
|
+
hash[path_component]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def fuzzy_find_path(path)
|
124
|
+
name = ::File.basename(path)
|
125
|
+
tree = get ::File.dirname(path)
|
126
|
+
if tree && tree.is_a?(FileTree)
|
127
|
+
tree.each do |filename, contents|
|
128
|
+
next unless contents.is_a?(File)
|
129
|
+
extension = ::File.extname(filename)
|
130
|
+
|
131
|
+
if name == ::File.basename(filename, extension)
|
132
|
+
return "#{path}#{extension}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def write_hash_to_path(path, hash)
|
140
|
+
FileUtils.mkdir_p path
|
141
|
+
|
142
|
+
hash.each do |name, content|
|
143
|
+
this_path = ::File.join path, name
|
144
|
+
|
145
|
+
if content.is_a? Hash
|
146
|
+
write_hash_to_path this_path, content
|
147
|
+
else
|
148
|
+
::File.open(this_path, "w") { |f| f.write content }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
class Ignorer
|
5
|
+
PATTERNS = [
|
6
|
+
/^\.DS_Store$/,
|
7
|
+
/^.*~$/,
|
8
|
+
/^#.*\#$/ #/(comment fixes broken vim highlighting)
|
9
|
+
]
|
10
|
+
|
11
|
+
def initialize(tree)
|
12
|
+
@input = tree
|
13
|
+
@output = FileTree.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def result
|
17
|
+
@path = []
|
18
|
+
@input.accept(self)
|
19
|
+
@output
|
20
|
+
end
|
21
|
+
|
22
|
+
def enter_tree(tree)
|
23
|
+
@path << tree.name if tree.name
|
24
|
+
end
|
25
|
+
|
26
|
+
def leave_tree(tree)
|
27
|
+
@path.pop if tree.name
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_file(file)
|
31
|
+
file_path = [*@path, file.path].join("/")
|
32
|
+
|
33
|
+
unless PATTERNS.any? { |p| p.match(file.path) }
|
34
|
+
@output.write file_path, file.content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
class JsMinifier
|
5
|
+
def initialize(tree)
|
6
|
+
require "uglifier"
|
7
|
+
@input = tree
|
8
|
+
@output = FileTree.new
|
9
|
+
@uglifier = Uglifier.new
|
10
|
+
rescue LoadError => e
|
11
|
+
raise "'gem install uglifier' to minify JavaScript files."
|
12
|
+
end
|
13
|
+
|
14
|
+
def result
|
15
|
+
@path = []
|
16
|
+
@input.accept(self)
|
17
|
+
@output
|
18
|
+
end
|
19
|
+
|
20
|
+
def enter_tree(tree)
|
21
|
+
@path << tree.name if tree.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def leave_tree(tree)
|
25
|
+
@path.pop if tree.name
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_file(file)
|
29
|
+
file_path = [*@path, file.path].join("/")
|
30
|
+
content = if file.extension == ".js"
|
31
|
+
@uglifier.compile(file.content)
|
32
|
+
else
|
33
|
+
file.content
|
34
|
+
end
|
35
|
+
@output.write file_path, content
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/bunch/middleware.rb
CHANGED
@@ -1,83 +1,32 @@
|
|
1
|
-
#
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "rack"
|
2
4
|
|
3
5
|
module Bunch
|
4
6
|
class Middleware
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(app, options={})
|
8
|
-
unless options[:root_url] && options[:path]
|
9
|
-
raise "Must provide :root_url and :path"
|
10
|
-
end
|
11
|
-
|
7
|
+
def initialize(app, paths_and_options)
|
12
8
|
@app = app
|
13
|
-
@
|
14
|
-
@endpoint = Bunch::Rack.new(options.delete(:path), options)
|
15
|
-
|
16
|
-
if options[:gzip]
|
17
|
-
@endpoint = ::Rack::Deflater.new(@endpoint)
|
18
|
-
end
|
9
|
+
@url_map = Rack::Deflater.new(build_url_map(paths_and_options))
|
19
10
|
end
|
20
11
|
|
21
12
|
def call(env)
|
22
|
-
|
23
|
-
script_name = env['SCRIPT_NAME'].to_s
|
24
|
-
|
25
|
-
if path =~ root_regexp &&
|
26
|
-
(rest = $1) &&
|
27
|
-
(rest.empty? || rest[0] == ?/)
|
13
|
+
response = @url_map.call(env)
|
28
14
|
|
29
|
-
|
30
|
-
env.merge(
|
31
|
-
'SCRIPT_NAME' => (script_name + @root_url),
|
32
|
-
'PATH_INFO' => rest
|
33
|
-
)
|
34
|
-
)
|
35
|
-
else
|
15
|
+
if response[1]["X-Cascade"] == "pass"
|
36
16
|
@app.call(env)
|
17
|
+
else
|
18
|
+
response
|
37
19
|
end
|
38
20
|
end
|
39
21
|
|
40
|
-
|
41
|
-
def root_regexp
|
42
|
-
Regexp.new("^#{Regexp.quote(@root_url).gsub('/', '/+')}(.*)")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class << Middleware
|
47
|
-
# Binds an instance of `Bunch::Middleware` in front of any instance
|
48
|
-
# of the given middleware class. Whenever `klass` is instantiated,
|
49
|
-
# any calls to `klass#call` will first pass through the single
|
50
|
-
# instance of `Middleware` created by this method.
|
51
|
-
#
|
52
|
-
# You can compose any number of `Middleware` instances in front
|
53
|
-
# of the same class.
|
54
|
-
#
|
55
|
-
# In practice, the purpose of this is to allow Bunch to take
|
56
|
-
# precedence over `Rails::Rack::Static` in a Rails 2 context.
|
57
|
-
# Since the server's instance of `Rails::Rack::Static` isn't part of
|
58
|
-
# the `ActionController` stack, it isn't possible to use
|
59
|
-
# Rails' normal middleware tools to insert Bunch in front of it,
|
60
|
-
# which means any files in `public` would be served preferentially.
|
61
|
-
#
|
62
|
-
# @param [Class] klass Any Rack middleware class.
|
63
|
-
# @param [Hash] options The options that would normally be passed to
|
64
|
-
# `Bunch::Middleware#initialize`.
|
65
|
-
#
|
66
|
-
# @example
|
67
|
-
# if Rails.env.development?
|
68
|
-
# Bunch::Middleware.insert_before Rails::Rack::Static,
|
69
|
-
# root_url: '/javascripts', path: 'app/scripts', no_cache: true
|
70
|
-
# end
|
71
|
-
def insert_before(klass, options={})
|
72
|
-
instance = new(nil, options)
|
22
|
+
private
|
73
23
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
24
|
+
def build_url_map(paths_and_options)
|
25
|
+
paths, options = paths_and_options.partition { |k, _| String === k }
|
26
|
+
mapping = Hash[paths.map do |url, directory|
|
27
|
+
[url, Server.new(Hash[options].merge(root: directory))]
|
28
|
+
end]
|
29
|
+
Rack::URLMap.new(mapping)
|
81
30
|
end
|
82
31
|
end
|
83
32
|
end
|