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.
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