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,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
@@ -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
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Bunch
2
- VERSION = "0.2.2"
4
+ VERSION = "1.0.0pre1"
3
5
  end
@@ -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