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,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Bunch
4
+ class File
5
+ attr_reader :path, :content, :filename, :extension
6
+
7
+ def initialize(path, content)
8
+ @path = path
9
+ @content = content
10
+ @filename = ::File.basename(@path)
11
+ @extension = ::File.extname(@filename)
12
+ end
13
+
14
+ def accept(visitor)
15
+ visitor.visit_file(self)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,159 @@
1
+ # encoding: UTF-8
2
+
3
+ require "digest/md5"
4
+ require "fileutils"
5
+ require "yaml"
6
+
7
+ module Bunch
8
+ def self.FileCache(*args)
9
+ FileCache.new(*args)
10
+ end
11
+
12
+ class FileCache
13
+ Result = Struct.new(:result)
14
+
15
+ def initialize(processor_class, input_dir, *args)
16
+ @processor_class, @args = processor_class, args
17
+ @cache_name = "#{processor_class}-#{input_dir}"
18
+ end
19
+
20
+ def new(tree)
21
+ cache = load_cache
22
+ partition = Partition.new(tree, cache)
23
+
24
+ partition.process!
25
+
26
+ processor = @processor_class.new(partition.pending, *@args)
27
+ result = TreeMerge.new(partition.cached, processor.result).result
28
+
29
+ save_cache tree, result
30
+ Result.new(result)
31
+ end
32
+
33
+ private
34
+
35
+ def load_cache
36
+ FileUtils.mkdir_p cache_directory
37
+ Cache.read_from_file cache_path
38
+ end
39
+
40
+ def save_cache(input, output)
41
+ FileUtils.mkdir_p cache_directory
42
+ Cache.from_trees(input, output).write_to_file cache_path
43
+ end
44
+
45
+ # TODO: Make configurable
46
+ def cache_directory
47
+ ".bunch-cache"
48
+ end
49
+
50
+ def cache_filename
51
+ @cache_name.gsub(/[:\/]/, "-")
52
+ end
53
+
54
+ def cache_path
55
+ ::File.join(cache_directory, cache_filename)
56
+ end
57
+
58
+ class Partition
59
+ attr_reader :pending, :cached
60
+
61
+ def initialize(input, cache)
62
+ @input = input
63
+ @cache = cache
64
+ end
65
+
66
+ def process!
67
+ @path = []
68
+ @pending = FileTree.new
69
+ @cached = FileTree.new
70
+ @input.accept(self)
71
+ self
72
+ end
73
+
74
+ def enter_tree(tree)
75
+ @path << tree.name if tree.name
76
+ end
77
+
78
+ def leave_tree(tree)
79
+ @path.pop if tree.name
80
+ end
81
+
82
+ def visit_file(file)
83
+ file_path = [*@path, file.path].join("/")
84
+ cached_content = @cache.read(file_path, file.content)
85
+ if cached_content
86
+ @cached.write file_path, cached_content
87
+ else
88
+ @pending.write file_path, file.content
89
+ end
90
+ end
91
+ end
92
+
93
+ class Cache
94
+ def self.from_trees(input, output)
95
+ new(HashHasher.new(input).result, output)
96
+ end
97
+
98
+ def self.read_from_file(path)
99
+ version, hashes, output = YAML.load_file(path)
100
+ if version == VERSION
101
+ new(hashes, FileTree.from_hash(output))
102
+ else
103
+ empty
104
+ end
105
+ rescue Errno::ENOENT
106
+ empty
107
+ end
108
+
109
+ def self.empty
110
+ new({}, FileTree.new)
111
+ end
112
+
113
+ def self.digest(contents)
114
+ Digest::MD5.hexdigest(contents)
115
+ end
116
+
117
+ def initialize(hashes, output)
118
+ @hashes, @output = hashes, output
119
+ end
120
+
121
+ def read(path, input_contents)
122
+ if Cache.digest(input_contents) == @hashes[path]
123
+ @output.get(path).content
124
+ end
125
+ end
126
+
127
+ def write_to_file(path)
128
+ yaml = YAML.dump([Bunch::VERSION, @hashes, @output.to_hash])
129
+ ::File.open(path, "w") { |f| f.write yaml }
130
+ end
131
+
132
+ class HashHasher
133
+ def initialize(tree)
134
+ @input = tree
135
+ end
136
+
137
+ def result
138
+ @path = []
139
+ @hashes = {}
140
+ @input.accept(self)
141
+ @hashes
142
+ end
143
+
144
+ def enter_tree(tree)
145
+ @path << tree.name if tree.name
146
+ end
147
+
148
+ def leave_tree(tree)
149
+ @path.pop if tree.name
150
+ end
151
+
152
+ def visit_file(file)
153
+ file_path = [*@path, file.path].join("/")
154
+ @hashes[file_path] = Digest::MD5.hexdigest(file.content)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+
3
+ require "fileutils"
4
+
5
+ module Bunch
6
+ class FileTree
7
+ include Enumerable
8
+
9
+ attr_reader :path, :name
10
+
11
+ class << self
12
+ def from_hash(hash)
13
+ new nil, hash
14
+ end
15
+
16
+ def from_path(path)
17
+ new nil, hash_from_path(path)
18
+ end
19
+
20
+ private
21
+
22
+ def hash_from_path(path)
23
+ hash = {}
24
+ Dir.foreach(path) do |entry|
25
+ next if entry == "." || entry == ".."
26
+ this_path = "#{path}/#{entry}"
27
+ if ::File.directory? this_path
28
+ hash[entry] = hash_from_path this_path
29
+ else
30
+ hash[entry] = ::File.read this_path
31
+ end
32
+ end
33
+ hash
34
+ end
35
+ end
36
+
37
+ def initialize(path = nil, hash = {})
38
+ @path = path
39
+ @hash = hash
40
+ @name = ::File.basename(@path) if @path
41
+ end
42
+
43
+ def directory?(path)
44
+ look_up_path(path).is_a?(Hash)
45
+ end
46
+
47
+ def exist?(filename)
48
+ !!look_up_path(filename)
49
+ end
50
+
51
+ def get(path)
52
+ case (content = look_up_path(path))
53
+ when Hash
54
+ FileTree.new(path, content)
55
+ when String
56
+ File.new(path, content)
57
+ end
58
+ end
59
+
60
+ # Like get, but takes a path with no extension and returns the first match
61
+ # regardless of its extension.
62
+ def get_fuzzy(fuzzy_path)
63
+ path = fuzzy_find_path(fuzzy_path)
64
+ get(path) if path
65
+ end
66
+
67
+ def write(path, contents)
68
+ dirname, _, filename = path.rpartition("/")
69
+
70
+ if dirname == ""
71
+ @hash[filename] = contents
72
+ else
73
+ unless exist?(dirname)
74
+ write(dirname, {})
75
+ end
76
+
77
+ unless directory?(dirname)
78
+ raise "#{dirname.inspect} is a file, not a directory!"
79
+ end
80
+
81
+ get(dirname).write(filename, contents)
82
+ end
83
+ end
84
+
85
+ def each
86
+ @hash.keys.each do |filename|
87
+ yield [filename, get(filename)]
88
+ end
89
+ end
90
+
91
+ def to_hash
92
+ @hash
93
+ end
94
+
95
+ def accept(visitor)
96
+ unless visitor.enter_tree(self) == false
97
+ each { |_, node| break if node.accept(visitor) == false }
98
+ end
99
+ visitor.leave_tree(self)
100
+ end
101
+
102
+ def write_to_path(path)
103
+ write_hash_to_path path, @hash
104
+ end
105
+
106
+ def ==(other)
107
+ to_hash == other.to_hash
108
+ end
109
+
110
+ def dup
111
+ FileTree.new(@path, Marshal.load(Marshal.dump(@hash)))
112
+ end
113
+
114
+ private
115
+
116
+ def look_up_path(filename)
117
+ filename.split("/").inject(@hash) do |hash, path_component|
118
+ break if hash.nil? || hash.is_a?(String)
119
+ hash[path_component]
120
+ end
121
+ end
122
+
123
+ def fuzzy_find_path(path)
124
+ name = ::File.basename(path)
125
+ tree = get ::File.dirname(path)
126
+ if tree && tree.is_a?(FileTree)
127
+ tree.each do |filename, contents|
128
+ next unless contents.is_a?(File)
129
+ extension = ::File.extname(filename)
130
+
131
+ if name == ::File.basename(filename, extension)
132
+ return "#{path}#{extension}"
133
+ end
134
+ end
135
+ nil
136
+ end
137
+ end
138
+
139
+ def write_hash_to_path(path, hash)
140
+ FileUtils.mkdir_p path
141
+
142
+ hash.each do |name, content|
143
+ this_path = ::File.join path, name
144
+
145
+ if content.is_a? Hash
146
+ write_hash_to_path this_path, content
147
+ else
148
+ ::File.open(this_path, "w") { |f| f.write content }
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ class Ignorer
5
+ PATTERNS = [
6
+ /^\.DS_Store$/,
7
+ /^.*~$/,
8
+ /^#.*\#$/ #/(comment fixes broken vim highlighting)
9
+ ]
10
+
11
+ def initialize(tree)
12
+ @input = tree
13
+ @output = FileTree.new
14
+ end
15
+
16
+ def result
17
+ @path = []
18
+ @input.accept(self)
19
+ @output
20
+ end
21
+
22
+ def enter_tree(tree)
23
+ @path << tree.name if tree.name
24
+ end
25
+
26
+ def leave_tree(tree)
27
+ @path.pop if tree.name
28
+ end
29
+
30
+ def visit_file(file)
31
+ file_path = [*@path, file.path].join("/")
32
+
33
+ unless PATTERNS.any? { |p| p.match(file.path) }
34
+ @output.write file_path, file.content
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunch
4
+ class JsMinifier
5
+ def initialize(tree)
6
+ require "uglifier"
7
+ @input = tree
8
+ @output = FileTree.new
9
+ @uglifier = Uglifier.new
10
+ rescue LoadError => e
11
+ raise "'gem install uglifier' to minify JavaScript files."
12
+ end
13
+
14
+ def result
15
+ @path = []
16
+ @input.accept(self)
17
+ @output
18
+ end
19
+
20
+ def enter_tree(tree)
21
+ @path << tree.name if tree.name
22
+ end
23
+
24
+ def leave_tree(tree)
25
+ @path.pop if tree.name
26
+ end
27
+
28
+ def visit_file(file)
29
+ file_path = [*@path, file.path].join("/")
30
+ content = if file.extension == ".js"
31
+ @uglifier.compile(file.content)
32
+ else
33
+ file.content
34
+ end
35
+ @output.write file_path, content
36
+ end
37
+ end
38
+ end
@@ -1,83 +1,32 @@
1
- # heavily inspired by Rack::URLMap
1
+ # encoding: UTF-8
2
+
3
+ require "rack"
2
4
 
3
5
  module Bunch
4
6
  class Middleware
5
- attr_accessor :app, :endpoint
6
-
7
- def initialize(app, options={})
8
- unless options[:root_url] && options[:path]
9
- raise "Must provide :root_url and :path"
10
- end
11
-
7
+ def initialize(app, paths_and_options)
12
8
  @app = app
13
- @root_url = options.delete(:root_url)
14
- @endpoint = Bunch::Rack.new(options.delete(:path), options)
15
-
16
- if options[:gzip]
17
- @endpoint = ::Rack::Deflater.new(@endpoint)
18
- end
9
+ @url_map = Rack::Deflater.new(build_url_map(paths_and_options))
19
10
  end
20
11
 
21
12
  def call(env)
22
- path = env['PATH_INFO'].to_s
23
- script_name = env['SCRIPT_NAME'].to_s
24
-
25
- if path =~ root_regexp &&
26
- (rest = $1) &&
27
- (rest.empty? || rest[0] == ?/)
13
+ response = @url_map.call(env)
28
14
 
29
- @endpoint.call(
30
- env.merge(
31
- 'SCRIPT_NAME' => (script_name + @root_url),
32
- 'PATH_INFO' => rest
33
- )
34
- )
35
- else
15
+ if response[1]["X-Cascade"] == "pass"
36
16
  @app.call(env)
17
+ else
18
+ response
37
19
  end
38
20
  end
39
21
 
40
- protected
41
- def root_regexp
42
- Regexp.new("^#{Regexp.quote(@root_url).gsub('/', '/+')}(.*)")
43
- end
44
- end
45
-
46
- class << Middleware
47
- # Binds an instance of `Bunch::Middleware` in front of any instance
48
- # of the given middleware class. Whenever `klass` is instantiated,
49
- # any calls to `klass#call` will first pass through the single
50
- # instance of `Middleware` created by this method.
51
- #
52
- # You can compose any number of `Middleware` instances in front
53
- # of the same class.
54
- #
55
- # In practice, the purpose of this is to allow Bunch to take
56
- # precedence over `Rails::Rack::Static` in a Rails 2 context.
57
- # Since the server's instance of `Rails::Rack::Static` isn't part of
58
- # the `ActionController` stack, it isn't possible to use
59
- # Rails' normal middleware tools to insert Bunch in front of it,
60
- # which means any files in `public` would be served preferentially.
61
- #
62
- # @param [Class] klass Any Rack middleware class.
63
- # @param [Hash] options The options that would normally be passed to
64
- # `Bunch::Middleware#initialize`.
65
- #
66
- # @example
67
- # if Rails.env.development?
68
- # Bunch::Middleware.insert_before Rails::Rack::Static,
69
- # root_url: '/javascripts', path: 'app/scripts', no_cache: true
70
- # end
71
- def insert_before(klass, options={})
72
- instance = new(nil, options)
22
+ private
73
23
 
74
- klass.class_eval do
75
- unbound_call = instance_method(:call)
76
- define_method(:call) do |env|
77
- instance.app = unbound_call.bind(self)
78
- instance.call(env)
79
- end
80
- end
24
+ def build_url_map(paths_and_options)
25
+ paths, options = paths_and_options.partition { |k, _| String === k }
26
+ mapping = Hash[paths.map do |url, directory|
27
+ [url, Server.new(Hash[options].merge(root: directory))]
28
+ end]
29
+ Rack::URLMap.new(mapping)
81
30
  end
82
31
  end
83
32
  end