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