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