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
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
class Pipeline
|
5
|
+
ENVIRONMENTS = {}
|
6
|
+
|
7
|
+
def self.define(environment, &block)
|
8
|
+
ENVIRONMENTS[environment.to_s] = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.for_environment(config)
|
12
|
+
environment = config.fetch(:environment)
|
13
|
+
proc = ENVIRONMENTS[environment] ||
|
14
|
+
raise("No pipeline defined for #{environment}!")
|
15
|
+
new(proc.call(config))
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(processors)
|
19
|
+
@processors = processors
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(input_tree)
|
23
|
+
tree = input_tree
|
24
|
+
@processors.each do |processor|
|
25
|
+
tree = processor.new(tree).result
|
26
|
+
end
|
27
|
+
tree
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/bunch/server.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "mime/types"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
class Server
|
7
|
+
def initialize(config)
|
8
|
+
Bunch.load_default_config_if_possible
|
9
|
+
|
10
|
+
@reader = config.fetch(:reader) do
|
11
|
+
proc { FileTree.from_path(config.fetch(:root)) }
|
12
|
+
end
|
13
|
+
|
14
|
+
@pipeline = config.fetch(:pipeline) do
|
15
|
+
Pipeline.for_environment config
|
16
|
+
end
|
17
|
+
|
18
|
+
@headers = config.fetch(:headers) do
|
19
|
+
{
|
20
|
+
"Cache-Control" => "private, max-age=0, must-revalidate",
|
21
|
+
"Pragma" => "no-cache",
|
22
|
+
"Expires" => "Thu, 01 Dec 1994 16:00:00 GMT"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
path = env["PATH_INFO"].sub(/^\//, '')
|
29
|
+
type = mime_type_for_path(path)
|
30
|
+
tree = @pipeline.process(@reader.call)
|
31
|
+
file = tree.get(path)
|
32
|
+
|
33
|
+
if file.is_a?(File)
|
34
|
+
[200, headers_for_type(type), [file.content]]
|
35
|
+
else
|
36
|
+
[404, headers_for_type("text/plain"), ["Not Found"]]
|
37
|
+
end
|
38
|
+
rescue => e
|
39
|
+
[500, headers_for_type("text/plain"), [error_message(e)]]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def mime_type_for_path(path)
|
45
|
+
MIME::Types.type_for(path).first || "text/plain"
|
46
|
+
end
|
47
|
+
|
48
|
+
def headers_for_type(mime_type)
|
49
|
+
@headers.merge("Content-Type" => mime_type.to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
def error_message(e)
|
53
|
+
"#{e.class}: #{e.message}\n #{e.backtrace.join("\n ")}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
def self.SimpleCache(*args)
|
5
|
+
SimpleCache.new(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
class SimpleCache
|
9
|
+
Result = Struct.new(:result)
|
10
|
+
|
11
|
+
def initialize(processor_class, *args)
|
12
|
+
@processor_class, @args = processor_class, args
|
13
|
+
@cache = nil
|
14
|
+
@hache = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def new(tree)
|
18
|
+
check_cache!(tree)
|
19
|
+
@cache ||= begin
|
20
|
+
processor = @processor_class.new(tree, *@args)
|
21
|
+
processor.result
|
22
|
+
end
|
23
|
+
Result.new(@cache.dup)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def check_cache!(tree)
|
29
|
+
hash = ContentHash.new(tree).result
|
30
|
+
if hash != @hache
|
31
|
+
@cache = nil
|
32
|
+
@hache = hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Bunch
|
4
|
+
class TreeMerge
|
5
|
+
def initialize(left, right)
|
6
|
+
@left, @right = left, right
|
7
|
+
end
|
8
|
+
|
9
|
+
def result
|
10
|
+
@path = []
|
11
|
+
@output = @right.dup
|
12
|
+
@left.accept(self)
|
13
|
+
@output
|
14
|
+
end
|
15
|
+
|
16
|
+
def enter_tree(tree)
|
17
|
+
@path << tree.name if tree.name
|
18
|
+
end
|
19
|
+
|
20
|
+
def leave_tree(tree)
|
21
|
+
@path.pop if tree.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_file(file)
|
25
|
+
file_path = [*@path, file.path].join("/")
|
26
|
+
@output.write file_path, file.content
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/bunch/version.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
describe CLI do
|
7
|
+
describe ".run!" do
|
8
|
+
it "instantiates CLI with the given args and calls run! on it" do
|
9
|
+
args = %w(a b c)
|
10
|
+
cli = stub
|
11
|
+
cli.expects(:run!)
|
12
|
+
CLI.expects(:new).with(args).returns(cli)
|
13
|
+
CLI.run!(args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "requires both input and output paths" do
|
18
|
+
out = StringIO.new
|
19
|
+
CLI.new(%w(in_path), out).run!
|
20
|
+
out.string.must_include "Error:"
|
21
|
+
out.string.must_include "Usage:"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "prints usage" do
|
25
|
+
out = StringIO.new
|
26
|
+
CLI.new(%w(-h), out).run!
|
27
|
+
out.string.wont_include "Error:"
|
28
|
+
out.string.must_include "Usage:"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "loads the default config file if possible" do
|
32
|
+
::File.stubs(:exist?).with("config/bunch.rb").returns(true)
|
33
|
+
Bunch.expects(:load).with("config/bunch.rb")
|
34
|
+
run_example
|
35
|
+
end
|
36
|
+
|
37
|
+
it "loads an alternate config file if provided" do
|
38
|
+
::File.expects(:exist?).with("config/bunch.rb").never
|
39
|
+
Bunch.expects(:load).with("config/bunch.rb").never
|
40
|
+
::File.stubs(:exist?).with("config/foo.rb").returns(true)
|
41
|
+
Bunch.expects(:load).with("config/foo.rb")
|
42
|
+
|
43
|
+
run_example(["-c config/foo.rb"])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "errors out if given a non-existent config file" do
|
47
|
+
::File.stubs(:exist?).with("config/foo.rb").returns(false)
|
48
|
+
|
49
|
+
run_example(["-c config/foo.rb"]) do |tmpdir, out|
|
50
|
+
out.must_include "Error: cannot load such file"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "loads multiple config files" do
|
55
|
+
::File.stubs(:exist?).with("config/foo.rb").returns(true)
|
56
|
+
Bunch.expects(:load).with("config/foo.rb")
|
57
|
+
::File.stubs(:exist?).with("config/bar.rb").returns(true)
|
58
|
+
Bunch.expects(:load).with("config/bar.rb")
|
59
|
+
|
60
|
+
run_example(%w(-c config/foo.rb --config config/bar.rb)) do |_, out|
|
61
|
+
out.must_equal ""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "runs the given environment's pipeline on the given path" do
|
66
|
+
run_example do |tmpdir, out|
|
67
|
+
out.must_equal ""
|
68
|
+
FileTree.from_path(tmpdir).to_hash.must_equal(
|
69
|
+
"directory" => "2\n\n1\n",
|
70
|
+
"file3" => "3\n"
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_example(params = nil)
|
76
|
+
Dir.mktmpdir do |tmpdir|
|
77
|
+
out = StringIO.new
|
78
|
+
params ||= ["-e development"]
|
79
|
+
params = [*params, "spec/example_tree", tmpdir]
|
80
|
+
CLI.new(params, out).run!
|
81
|
+
yield(tmpdir, out.string) if block_given?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
describe Combiner do
|
7
|
+
def self.scenario(name, input, output)
|
8
|
+
it "combines a tree correctly: #{name}" do
|
9
|
+
result_for_hash(input).must_equal output
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def result_for_hash(input)
|
14
|
+
Combiner.new(FileTree.from_hash(input)).result.to_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
scenario "one JavaScript file",
|
18
|
+
{"a.js" => "hello;"},
|
19
|
+
{"a.js" => "hello;"}
|
20
|
+
|
21
|
+
scenario "two JavaScript files in a directory",
|
22
|
+
{"a" => {"b.js" => "hello;", "c.js" => "goodbye;"}},
|
23
|
+
{"a" => {"b.js" => "hello;", "c.js" => "goodbye;"}}
|
24
|
+
|
25
|
+
scenario "two JavaScript files and a _combine file",
|
26
|
+
{"a" => {"_combine" => "", "b.js" => "hello;", "c.js" => "goodbye;"}},
|
27
|
+
{"a.js" => "hello;\ngoodbye;"}
|
28
|
+
|
29
|
+
scenario "ignoring a _combine file at the top level",
|
30
|
+
{"_combine" => "", "a" => {"b.js" => "bar;", "c.js" => "baz;"}},
|
31
|
+
{"a" => {"b.js" => "bar;", "c.js" => "baz;"}}
|
32
|
+
|
33
|
+
scenario "a more complex tree of JavaScript files",
|
34
|
+
{"a" => {
|
35
|
+
"b" => {
|
36
|
+
"_combine" => "", "c.js" => "hello;", "d.js" => "goodbye;"
|
37
|
+
},
|
38
|
+
"e.js" => "and_another_thing;"
|
39
|
+
}},
|
40
|
+
{"a" => {
|
41
|
+
"b.js" => "hello;\ngoodbye;",
|
42
|
+
"e.js" => "and_another_thing;"
|
43
|
+
}}
|
44
|
+
|
45
|
+
scenario "_combine forces subtrees to collapse",
|
46
|
+
{"a" => {
|
47
|
+
"b" => {
|
48
|
+
"_combine" => "",
|
49
|
+
"c" => {
|
50
|
+
"d.js" => "whoops;",
|
51
|
+
"e.js" => "it_worked;"
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}},
|
55
|
+
{"a" => {"b.js" => "whoops;\nit_worked;"}}
|
56
|
+
|
57
|
+
scenario "obey ordering found in _combine",
|
58
|
+
{"a" => {
|
59
|
+
"d" => "pants",
|
60
|
+
"a" => "spigot",
|
61
|
+
"_combine" => "c\nd\n",
|
62
|
+
"c" => "deuce",
|
63
|
+
"b" => "rands"
|
64
|
+
}},
|
65
|
+
{"a" => "deuce\npants\nspigot\nrands"}
|
66
|
+
|
67
|
+
scenario "deal with nested combines",
|
68
|
+
{"a" => {
|
69
|
+
"_combine" => "e\nb\n",
|
70
|
+
"b" => {
|
71
|
+
"_combine" => "d\nc\n",
|
72
|
+
"c.js" => "stuff",
|
73
|
+
"d.js" => "other_stuff"
|
74
|
+
},
|
75
|
+
"e.js" => "still_more_stuff"
|
76
|
+
}},
|
77
|
+
{"a.js" => "still_more_stuff\nother_stuff\nstuff"}
|
78
|
+
|
79
|
+
scenario "deal with inner directory with no _combine",
|
80
|
+
{"a" => {
|
81
|
+
"_combine" => "e\nb\n",
|
82
|
+
"b.js" => "still_more_stuff",
|
83
|
+
"e" => {
|
84
|
+
"c.js" => "stuff",
|
85
|
+
"d.js" => "other_stuff"
|
86
|
+
}
|
87
|
+
}},
|
88
|
+
{"a.js" => "stuff\nother_stuff\nstill_more_stuff"}
|
89
|
+
|
90
|
+
scenario "deal with empty directory",
|
91
|
+
{"a" => {
|
92
|
+
"b" => {
|
93
|
+
"_combine" => "",
|
94
|
+
},
|
95
|
+
"e.js" => "stuff"
|
96
|
+
}},
|
97
|
+
{"a" => {"e.js" => "stuff"}}
|
98
|
+
|
99
|
+
it "raises when one combine has incompatible files" do
|
100
|
+
proc do
|
101
|
+
result_for_hash(
|
102
|
+
"a" => { "_combine" => "", "a.js" => "", "b.css" => "" }
|
103
|
+
)
|
104
|
+
end.must_raise RuntimeError
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
describe Compiler do
|
7
|
+
it "passes plain files through unchanged" do
|
8
|
+
hash = {
|
9
|
+
"a" => { "b" => "c", "d" => "e" },
|
10
|
+
"f" => "g"
|
11
|
+
}
|
12
|
+
result = Compiler.new(FileTree.from_hash(hash)).result.to_hash
|
13
|
+
result.must_equal hash
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "when a file type has a registered compiler" do
|
17
|
+
before do
|
18
|
+
compiler = Class.new do
|
19
|
+
def initialize(*)
|
20
|
+
end
|
21
|
+
|
22
|
+
def path
|
23
|
+
"b.js"
|
24
|
+
end
|
25
|
+
|
26
|
+
def content
|
27
|
+
"!!!"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Compiler.register ".foobar", compiler
|
32
|
+
end
|
33
|
+
|
34
|
+
after do
|
35
|
+
Compiler.compilers.delete ".foobar"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "uses the compiler to transform a file of that type" do
|
39
|
+
input = {
|
40
|
+
"a" => { "b.foobar" => "c", "d.js" => "e" },
|
41
|
+
"f" => "g"
|
42
|
+
}
|
43
|
+
output = {
|
44
|
+
"a" => { "b.js" => "!!!", "d.js" => "e" },
|
45
|
+
"f" => "g"
|
46
|
+
}
|
47
|
+
result = Compiler.new(FileTree.from_hash(input)).result.to_hash
|
48
|
+
result.must_equal output
|
49
|
+
end
|
50
|
+
|
51
|
+
it "passes the file, tree, and path into the compiler" do
|
52
|
+
input = { "a" => { "b.foobar" => "c" } }
|
53
|
+
|
54
|
+
Compiler.compilers[".foobar"].expects(:new).with do |file, tree, path|
|
55
|
+
file.path.must_equal "b.foobar"
|
56
|
+
file.content.must_equal "c"
|
57
|
+
file.filename.must_equal "b.foobar"
|
58
|
+
file.extension.must_equal ".foobar"
|
59
|
+
tree.to_hash.must_equal input
|
60
|
+
path.must_equal "a/b.foobar"
|
61
|
+
end.returns(stub(path: "b.js", content: "!!!"))
|
62
|
+
|
63
|
+
Compiler.new(FileTree.from_hash(input)).result
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can register a replacement compiler" do
|
67
|
+
o = Object.new
|
68
|
+
Compiler.register ".foobar", o
|
69
|
+
Compiler.compilers[".foobar"].must_equal o
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Bunch
|
6
|
+
module Compilers
|
7
|
+
describe CoffeeScript do
|
8
|
+
it "compiles a CoffeeScript file to JS" do
|
9
|
+
file = File.new("a/my_file.coffee", "@a = 10")
|
10
|
+
compiler = CoffeeScript.new(file)
|
11
|
+
compiler.path.must_equal "a/my_file.js"
|
12
|
+
compiler.content.must_equal \
|
13
|
+
"(function() {\n\n this.a = 10;\n\n}).call(this);\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "raises if the gem isn't available" do
|
17
|
+
CoffeeScript.any_instance.stubs(:require).raises(LoadError)
|
18
|
+
exception = assert_raises(RuntimeError) { CoffeeScript.new(nil) }
|
19
|
+
exception.message.must_include "gem install"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|