bunch 0.2.2 → 1.0.0pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -6
  3. data/Gemfile +3 -1
  4. data/Guardfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/Rakefile +7 -12
  7. data/bin/bunch +2 -5
  8. data/bunch.gemspec +30 -23
  9. data/lib/bunch.rb +37 -81
  10. data/lib/bunch/cli.rb +40 -74
  11. data/lib/bunch/combiner.rb +121 -0
  12. data/lib/bunch/compiler.rb +52 -0
  13. data/lib/bunch/compilers/coffee_script.rb +23 -0
  14. data/lib/bunch/compilers/ejs.rb +28 -0
  15. data/lib/bunch/compilers/jade.rb +28 -0
  16. data/lib/bunch/compilers/jst.rb +38 -0
  17. data/lib/bunch/compilers/null.rb +19 -0
  18. data/lib/bunch/compilers/sass.rb +55 -0
  19. data/lib/bunch/content_hash.rb +37 -0
  20. data/lib/bunch/css_minifier.rb +121 -0
  21. data/lib/bunch/file.rb +18 -0
  22. data/lib/bunch/file_cache.rb +159 -0
  23. data/lib/bunch/file_tree.rb +153 -0
  24. data/lib/bunch/ignorer.rb +38 -0
  25. data/lib/bunch/js_minifier.rb +38 -0
  26. data/lib/bunch/middleware.rb +16 -67
  27. data/lib/bunch/pipeline.rb +30 -0
  28. data/lib/bunch/server.rb +56 -0
  29. data/lib/bunch/simple_cache.rb +36 -0
  30. data/lib/bunch/tree_merge.rb +29 -0
  31. data/lib/bunch/version.rb +3 -1
  32. data/spec/bunch/cli_spec.rb +85 -0
  33. data/spec/bunch/combiner_spec.rb +107 -0
  34. data/spec/bunch/compiler_spec.rb +73 -0
  35. data/spec/bunch/compilers/coffee_script_spec.rb +23 -0
  36. data/spec/bunch/compilers/ejs_spec.rb +27 -0
  37. data/spec/bunch/compilers/jade_spec.rb +28 -0
  38. data/spec/bunch/compilers/sass_spec.rb +120 -0
  39. data/spec/bunch/css_minifier_spec.rb +31 -0
  40. data/spec/bunch/file_cache_spec.rb +151 -0
  41. data/spec/bunch/file_tree_spec.rb +127 -0
  42. data/spec/bunch/ignorer_spec.rb +26 -0
  43. data/spec/bunch/js_minifier_spec.rb +35 -0
  44. data/spec/bunch/middleware_spec.rb +41 -0
  45. data/spec/bunch/pipeline_spec.rb +31 -0
  46. data/spec/bunch/server_spec.rb +90 -0
  47. data/spec/bunch/simple_cache_spec.rb +55 -0
  48. data/spec/bunch/tree_merge_spec.rb +30 -0
  49. data/spec/bunch_spec.rb +6 -0
  50. data/spec/example_tree/directory/_combine +2 -0
  51. data/{example/js/test1.js → spec/example_tree/directory/file1} +0 -0
  52. data/{example/js/test2/test2a.js → spec/example_tree/directory/file2} +0 -0
  53. data/{example/js/test2/test2c.js → spec/example_tree/file3} +0 -0
  54. data/spec/spec_helper.rb +38 -0
  55. metadata +224 -102
  56. data/.yardopts +0 -1
  57. data/README.md +0 -4
  58. data/config.ru +0 -6
  59. data/example/config.ru +0 -6
  60. data/example/css/test1.css +0 -1
  61. data/example/css/test2/test2a.scss +0 -1
  62. data/example/css/test2/test2b.css +0 -1
  63. data/example/js/.bunchignore +0 -1
  64. data/example/js/test2/_.yml +0 -2
  65. data/example/js/test2/foo.js +0 -1
  66. data/example/js/test2/test2b.js +0 -1
  67. data/example/js/test3/test3a.js +0 -1
  68. data/example/js/test3/test3b/_.yml +0 -1
  69. data/example/js/test3/test3b/test3bi.js +0 -1
  70. data/example/js/test3/test3b/test3bii.js +0 -1
  71. data/example/js/test4/_.yml +0 -1
  72. data/example/js/test4/test4a.js +0 -1
  73. data/example/js/test4/test4b.coffee +0 -1
  74. data/example/js/test4/test4c.coffee +0 -1
  75. data/example/js/test5/test5a.jst.ejs +0 -1
  76. data/lib/bunch/abstract_node.rb +0 -25
  77. data/lib/bunch/cache.rb +0 -40
  78. data/lib/bunch/coffee_node.rb +0 -39
  79. data/lib/bunch/directory_node.rb +0 -82
  80. data/lib/bunch/ejs_node.rb +0 -50
  81. data/lib/bunch/file_node.rb +0 -25
  82. data/lib/bunch/jade_node.rb +0 -50
  83. data/lib/bunch/null_node.rb +0 -11
  84. data/lib/bunch/rack.rb +0 -38
  85. data/lib/bunch/sass_node.rb +0 -39
  86. data/test/middleware_test.rb +0 -26
  87. data/test/rack_test.rb +0 -93
  88. data/test/test_helper.rb +0 -21
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ module Bunch
6
+ module Compilers
7
+ describe EJS do
8
+ it "compiles an EJS file to JS" do
9
+ tree = FileTree.from_hash(
10
+ "a" => { "my_file.jst.ejs" => "<% hello %>" }
11
+ )
12
+ compiler = EJS.new(
13
+ tree.get("a").get("my_file.jst.ejs"), tree, "a/my_file.jst.ejs")
14
+ output = compiler.content
15
+
16
+ compiler.path.must_equal "a/my_file.js"
17
+ output.must_include "JST['a/my_file'] = function"
18
+ end
19
+
20
+ it "raises if the gem isn't available" do
21
+ EJS.any_instance.stubs(:require).raises(LoadError)
22
+ exception = assert_raises(RuntimeError) { EJS.new(nil, nil, nil) }
23
+ exception.message.must_include "gem install"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ module Bunch
6
+ module Compilers
7
+ describe Jade do
8
+ it "compiles a Jade file to JS" do
9
+ tree = FileTree.from_hash(
10
+ "a" => { "my_file.jst.jade" => "h1\n = hello\n hr\n" }
11
+ )
12
+ compiler = Jade.new(
13
+ tree.get("a").get("my_file.jst.jade"), tree, "a/my_file.jst.jade")
14
+ output = compiler.content
15
+
16
+ compiler.path.must_equal "a/my_file.js"
17
+ output.must_include "JST['a/my_file'] = function"
18
+ output.must_include "<hr/>"
19
+ end
20
+
21
+ it "raises if the gem isn't available" do
22
+ Jade.any_instance.stubs(:require).raises(LoadError)
23
+ exception = assert_raises(RuntimeError) { Jade.new(nil, nil, nil) }
24
+ exception.message.must_include "gem install"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ module Bunch
6
+ module Compilers
7
+ describe Sass do
8
+ it "compiles a .scss file to CSS" do
9
+ tree = FileTree.from_hash(
10
+ "a" => { "my_file.scss" => "div { span { width: 20px; } }" }
11
+ )
12
+ compiler = Sass.new(tree.get("a/my_file.scss"), tree, "a/my_file.scss")
13
+ compiler.path.must_equal "a/my_file.css"
14
+ compiler.content.must_equal "div span {\n width: 20px; }\n"
15
+ end
16
+
17
+ it "compiles a .sass file to CSS" do
18
+ tree = FileTree.from_hash(
19
+ "a" => { "my_file.sass" => "div\n span\n width: 20px\n" }
20
+ )
21
+ compiler = Sass.new(tree.get("a/my_file.sass"), tree, "a/my_file.sass")
22
+ compiler.path.must_equal "a/my_file.css"
23
+ compiler.content.must_equal "div span {\n width: 20px; }\n"
24
+ end
25
+
26
+ it "raises if the gem isn't available" do
27
+ Sass.any_instance.stubs(:require).raises(LoadError)
28
+ exception = assert_raises(RuntimeError) { Sass.new(nil, nil, nil) }
29
+ exception.message.must_include "gem install"
30
+ end
31
+
32
+ describe "dealing with @imports" do
33
+ let(:included_file) { "@mixin foobar { width: 20px; }" }
34
+
35
+ it "imports a file from the same directory" do
36
+ including_file = <<-SCSS
37
+ @import "included.scss";
38
+
39
+ div {
40
+ @include foobar;
41
+ }
42
+ SCSS
43
+
44
+ tree = FileTree.from_hash(
45
+ "a" => {
46
+ "including.scss" => including_file,
47
+ "included.scss" => included_file
48
+ }
49
+ )
50
+
51
+ compiler = Sass.new(
52
+ tree.get("a/including.scss"), tree, "a/including.scss")
53
+ compiler.path.must_equal "a/including.css"
54
+ compiler.content.must_equal "div {\n width: 20px; }\n"
55
+ end
56
+
57
+ it "imports a file with no extension (and a leading underscore)" do
58
+ including_file = <<-SCSS
59
+ @import "included";
60
+
61
+ div {
62
+ @include foobar;
63
+ }
64
+ SCSS
65
+
66
+ tree = FileTree.from_hash(
67
+ "a" => {
68
+ "including.scss" => including_file,
69
+ "_included.scss" => included_file
70
+ }
71
+ )
72
+
73
+ compiler = Sass.new(
74
+ tree.get("a/including.scss"), tree, "a/including.scss")
75
+ compiler.path.must_equal "a/including.css"
76
+ compiler.content.must_equal "div {\n width: 20px; }\n"
77
+ end
78
+
79
+ it "imports a file from a parent directory" do
80
+ including_file = <<-SCSS
81
+ @import "../included.scss";
82
+
83
+ div {
84
+ @include foobar;
85
+ }
86
+ SCSS
87
+
88
+ tree = FileTree.from_hash(
89
+ "a" => {
90
+ "b" => {
91
+ "including.scss" => including_file,
92
+ },
93
+ "included.scss" => included_file
94
+ }
95
+ )
96
+
97
+ compiler = Sass.new(
98
+ tree.get("a/b/including.scss"), tree, "a/b/including.scss")
99
+ compiler.path.must_equal "a/b/including.css"
100
+ compiler.content.must_equal "div {\n width: 20px; }\n"
101
+ end
102
+
103
+ it "has an informative error message if the import is missing" do
104
+ including_file = '@import "../included.scss";'
105
+
106
+ tree = FileTree.from_hash(
107
+ "a" => {
108
+ "b" => { "including.scss" => including_file }
109
+ }
110
+ )
111
+
112
+ compiler = Sass.new(
113
+ tree.get("a/b/including.scss"), tree, "a/b/including.scss")
114
+ error = proc { compiler.content }.must_raise RuntimeError
115
+ error.message.must_match /a\/included/
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ module Bunch
6
+ describe CssMinifier do
7
+ let(:file_contents) do
8
+ <<-CSS
9
+ body :hover {
10
+ border-left: 10px solid rgb(0, 0, 0);
11
+ }
12
+ CSS
13
+ end
14
+
15
+ let(:minified_contents) do
16
+ "body :hover{border-left:10px solid #000}"
17
+ end
18
+
19
+ let(:input_tree) do
20
+ FileTree.from_hash(
21
+ "a" => file_contents, "b" => { "c.css" => file_contents }
22
+ )
23
+ end
24
+
25
+ it "minifies .css files, ignoring other files" do
26
+ result = CssMinifier.new(input_tree).result.to_hash
27
+ result["a"].must_equal file_contents
28
+ result["b"]["c.css"].must_equal minified_contents
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,151 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+ require "fileutils"
5
+ require "tempfile"
6
+
7
+ module Bunch
8
+ describe FileCache do
9
+ let(:input_1) { FileTree.from_hash("a" => "1", "b" => "2") }
10
+ let(:input_2) { FileTree.from_hash("a" => "1", "b" => "3") }
11
+ let(:result_1) { FileTree.from_hash("a" => "!", "b" => "@") }
12
+ let(:result_2) { FileTree.from_hash("a" => "!", "b" => "#") }
13
+ let(:result_3) { FileTree.from_hash("a" => "%", "b" => "#") }
14
+ let(:processor_1) { stub new: stub(result: result_1), to_s: "processor" }
15
+ let(:processor_2) { stub new: stub(result: result_2), to_s: "processor" }
16
+ let(:processor_3) { stub new: stub(result: result_3), to_s: "processor" }
17
+ let(:processor_4) { stub new: stub(result: result_3), to_s: "grocessor" }
18
+
19
+ def new_cache(processor, path = "a_path")
20
+ FileCache.new(processor, path)
21
+ end
22
+
23
+ before do
24
+ FileUtils.rm_rf ".bunch-cache"
25
+ end
26
+
27
+ it "delegates to the underlying processor on a cold cache" do
28
+ new_cache(processor_1).new(input_1).result.must_equal result_1
29
+ end
30
+
31
+ it "returns the same results for the same input" do
32
+ new_cache(processor_1).new(input_1).result.must_equal result_1
33
+ new_cache(processor_2).new(input_1).result.must_equal result_1
34
+ end
35
+
36
+ it "returns different results for a different input" do
37
+ new_cache(processor_1).new(input_1).result.must_equal result_1
38
+ new_cache(processor_2).new(input_2).result.must_equal result_2
39
+ end
40
+
41
+ it "only updates paths that have changed" do
42
+ new_cache(processor_1).new(input_1).result.must_equal result_1
43
+ new_cache(processor_3).new(input_2).result.must_equal result_2
44
+ end
45
+
46
+ it "maintains distinct caches for different processors" do
47
+ new_cache(processor_1).new(input_1).result.must_equal result_1
48
+ new_cache(processor_4).new(input_1).result.must_equal result_3
49
+ end
50
+
51
+ it "maintains distinct caches for different input paths" do
52
+ new_cache(processor_1).new(input_1).result.must_equal result_1
53
+ new_cache(processor_2).new(input_1).result.must_equal result_1
54
+ new_cache(processor_2, "abc").new(input_1).result.must_equal result_2
55
+ end
56
+
57
+ it "considers the cache expired if Bunch's version changes" do
58
+ new_cache(processor_1).new(input_1).result.must_equal result_1
59
+ begin
60
+ FileCache::VERSION = "alsdkjalskdj"
61
+ new_cache(processor_2).new(input_1).result.must_equal result_2
62
+ ensure
63
+ FileCache.send :remove_const, :VERSION
64
+ end
65
+ end
66
+ end
67
+
68
+ class FileCache
69
+ describe Partition do
70
+ let(:empty) { FileTree.from_hash({}) }
71
+ let(:tree_1) { FileTree.from_hash("a" => {"b" => "1", "c" => "2"}) }
72
+
73
+ it "returns input tree for pending if cache is empty" do
74
+ cache = stub
75
+ cache.stubs(:read).returns(nil)
76
+ partition = Partition.new(tree_1, cache)
77
+ partition.process!
78
+ partition.cached.must_equal empty
79
+ partition.pending.must_equal tree_1
80
+ end
81
+
82
+ it "divides input tree into pending and cached" do
83
+ cache = stub
84
+ cache.stubs(:read).with("a/b", "1").returns("cache")
85
+ cache.stubs(:read).with("a/c", "2").returns(nil)
86
+ partition = Partition.new(tree_1, cache)
87
+ partition.process!
88
+ partition.cached.must_equal FileTree.from_hash("a" => {"b" => "cache"})
89
+ partition.pending.must_equal FileTree.from_hash("a" => {"c" => "2"})
90
+ end
91
+ end
92
+
93
+ describe Cache do
94
+ def create_cache
95
+ Cache.from_trees(
96
+ FileTree.from_hash("a" => "1", "b" => { "c" => "2" }),
97
+ FileTree.from_hash("a" => "!", "b" => { "c" => "@" }))
98
+ end
99
+
100
+ def assert_cache_is_correct(cache)
101
+ cache.read("a", "1").must_equal "!"
102
+ cache.read("a", "2").must_equal nil
103
+ cache.read("b/c", "2").must_equal "@"
104
+ cache.read("b/c/d", "2").must_equal nil
105
+ cache.read("b/d", "2").must_equal nil
106
+ end
107
+
108
+ it "records a mapping between two trees" do
109
+ cache = create_cache
110
+ assert_cache_is_correct cache
111
+ end
112
+
113
+ it "saves to and loads from a file" do
114
+ original_cache = create_cache
115
+ loaded_cache = nil
116
+
117
+ Tempfile.open(["cache", ".yml"]) do |tempfile|
118
+ tempfile.close
119
+ original_cache.write_to_file(tempfile.path)
120
+ loaded_cache = Cache.read_from_file(tempfile.path)
121
+ end
122
+ assert_cache_is_correct loaded_cache
123
+ end
124
+
125
+ it "returns a null cache if the file can't be opened" do
126
+ Cache.read_from_file("asldkasd").must_be_instance_of Cache
127
+ end
128
+
129
+ describe "#read" do
130
+ before do
131
+ @cache = Cache.new(
132
+ { "a" => Digest::MD5.hexdigest("1") },
133
+ FileTree.from_hash("a" => "!@#")
134
+ )
135
+ end
136
+
137
+ it "returns the result if the hash matches" do
138
+ @cache.read("a", "1").must_equal "!@#"
139
+ end
140
+
141
+ it "returns nil if the hash doesn't match" do
142
+ @cache.read("a", "2").must_equal nil
143
+ end
144
+
145
+ it "returns nil if the file isn't present" do
146
+ @cache.read("b", "1").must_equal nil
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ module Bunch
6
+ describe FileTree do
7
+ before do
8
+ @tree = FileTree.from_hash \
9
+ "a" => {
10
+ "b.txt" => "foo",
11
+ "c" => {
12
+ "d.js" => "bar",
13
+ "d.coffee" => "brr"
14
+ }
15
+ },
16
+ "e" => "baz"
17
+ end
18
+
19
+ describe ".from_path" do
20
+ it "loads a file hierarchy" do
21
+ path = ::File.expand_path "../../example_tree", __FILE__
22
+ tree = FileTree.from_path(path)
23
+ tree.to_hash.must_equal(
24
+ "directory" => {
25
+ "_combine" => "file2\nfile1\n",
26
+ "file1" => "1\n",
27
+ "file2" => "2\n"
28
+ },
29
+ "file3" => "3\n"
30
+ )
31
+ end
32
+ end
33
+
34
+ describe "#get" do
35
+ it "returns an object representing a file" do
36
+ file = @tree.get("e")
37
+ file.path.must_equal "e"
38
+ file.content.must_equal "baz"
39
+ file.extension.must_equal ""
40
+ end
41
+
42
+ it "returns an object representing a nested path" do
43
+ file = @tree.get("a/b.txt")
44
+ file.path.must_equal "a/b.txt"
45
+ file.content.must_equal "foo"
46
+ file.extension.must_equal ".txt"
47
+ end
48
+
49
+ it "returns an object representing a more nested path" do
50
+ file = @tree.get("a/c/d.js")
51
+ file.path.must_equal "a/c/d.js"
52
+ file.content.must_equal "bar"
53
+ file.extension.must_equal ".js"
54
+ end
55
+
56
+ it "returns nil if the path extends past a file" do
57
+ @tree.get("a/c/d.js/nonexistent").must_equal nil
58
+ end
59
+ end
60
+
61
+ describe "#get_fuzzy" do
62
+ it "returns an unambiguous match" do
63
+ file = @tree.get_fuzzy("a/b")
64
+ file.path.must_equal "a/b.txt"
65
+ file.content.must_equal "foo"
66
+ file.extension.must_equal ".txt"
67
+ end
68
+
69
+ it "returns the first of two matches" do
70
+ file = @tree.get_fuzzy("a/c/d")
71
+ file.path.must_equal "a/c/d.js"
72
+ file.content.must_equal "bar"
73
+ file.extension.must_equal ".js"
74
+ end
75
+
76
+ it "returns nil for no match" do
77
+ @tree.get_fuzzy("a/fff").must_equal nil
78
+ end
79
+
80
+ it "returns nil if the path extends past a file" do
81
+ @tree.get_fuzzy("a/c/d.js/nonexistent").must_equal nil
82
+ end
83
+ end
84
+
85
+ describe "#write" do
86
+ it "creates a file at the top level" do
87
+ @tree.write "f", "hello"
88
+ @tree.get("f").content.must_equal "hello"
89
+ end
90
+
91
+ it "creates a file in an existing directory" do
92
+ @tree.write "a/c/f", "hello"
93
+ @tree.get("a/c/f").content.must_equal "hello"
94
+ end
95
+
96
+ it "creates nested directories" do
97
+ @tree.write "a/c/a/f", "hello"
98
+ @tree.get("a/c/a/f").content.must_equal "hello"
99
+ end
100
+
101
+ it "overwrites an existing file" do
102
+ @tree.write "a/c/a/f", "hello"
103
+ @tree.write "a/c/a/f", "goodbye"
104
+ @tree.get("a/c/a/f").content.must_equal "goodbye"
105
+ end
106
+
107
+ it "raises if there's an existing file that conflicts with the path" do
108
+ proc {
109
+ @tree.write "a/c/d.js/f", "hello"
110
+ }.must_raise RuntimeError
111
+
112
+ @tree.get("a/c/d.js").content.must_equal "bar"
113
+ @tree.get("a/c/d.js/f").must_equal nil
114
+ end
115
+ end
116
+
117
+ describe "#write_to_path" do
118
+ it "mirrors the contents of the tree to the given path" do
119
+ Dir.mktmpdir do |tmpdir|
120
+ @tree.write_to_path tmpdir
121
+ ::File.read("#{tmpdir}/e").must_equal "baz"
122
+ ::File.read("#{tmpdir}/a/c/d.js").must_equal "bar"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end