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