rake-pipeline-fork 0.8.0

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 (75) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +12 -0
  5. data/.yardopts +2 -0
  6. data/GETTING_STARTED.md +268 -0
  7. data/Gemfile +14 -0
  8. data/LICENSE +20 -0
  9. data/README.markdown +11 -0
  10. data/README.yard +178 -0
  11. data/Rakefile +21 -0
  12. data/bin/rakep +4 -0
  13. data/examples/copying_files.md +12 -0
  14. data/examples/minifying_files.md +37 -0
  15. data/examples/modifying_pipelines.md +67 -0
  16. data/examples/multiple_pipelines.md +77 -0
  17. data/lib/generators/rake/pipeline/install/install_generator.rb +70 -0
  18. data/lib/rake-pipeline.rb +462 -0
  19. data/lib/rake-pipeline/cli.rb +56 -0
  20. data/lib/rake-pipeline/dsl.rb +9 -0
  21. data/lib/rake-pipeline/dsl/pipeline_dsl.rb +246 -0
  22. data/lib/rake-pipeline/dsl/project_dsl.rb +108 -0
  23. data/lib/rake-pipeline/dynamic_file_task.rb +194 -0
  24. data/lib/rake-pipeline/error.rb +17 -0
  25. data/lib/rake-pipeline/file_wrapper.rb +182 -0
  26. data/lib/rake-pipeline/filter.rb +249 -0
  27. data/lib/rake-pipeline/filters.rb +4 -0
  28. data/lib/rake-pipeline/filters/concat_filter.rb +63 -0
  29. data/lib/rake-pipeline/filters/gsub_filter.rb +56 -0
  30. data/lib/rake-pipeline/filters/ordering_concat_filter.rb +38 -0
  31. data/lib/rake-pipeline/filters/pipeline_finalizing_filter.rb +21 -0
  32. data/lib/rake-pipeline/graph.rb +178 -0
  33. data/lib/rake-pipeline/manifest.rb +86 -0
  34. data/lib/rake-pipeline/manifest_entry.rb +34 -0
  35. data/lib/rake-pipeline/matcher.rb +141 -0
  36. data/lib/rake-pipeline/middleware.rb +72 -0
  37. data/lib/rake-pipeline/precompile.rake +8 -0
  38. data/lib/rake-pipeline/project.rb +335 -0
  39. data/lib/rake-pipeline/rails_plugin.rb +10 -0
  40. data/lib/rake-pipeline/railtie.rb +34 -0
  41. data/lib/rake-pipeline/reject_matcher.rb +29 -0
  42. data/lib/rake-pipeline/server.rb +15 -0
  43. data/lib/rake-pipeline/sorted_pipeline.rb +19 -0
  44. data/lib/rake-pipeline/version.rb +6 -0
  45. data/rails/init.rb +2 -0
  46. data/rake-pipeline.gemspec +24 -0
  47. data/spec/cli_spec.rb +71 -0
  48. data/spec/concat_filter_spec.rb +37 -0
  49. data/spec/dsl/pipeline_dsl_spec.rb +165 -0
  50. data/spec/dsl/project_dsl_spec.rb +41 -0
  51. data/spec/dynamic_file_task_spec.rb +119 -0
  52. data/spec/encoding_spec.rb +106 -0
  53. data/spec/file_wrapper_spec.rb +132 -0
  54. data/spec/filter_spec.rb +332 -0
  55. data/spec/graph_spec.rb +56 -0
  56. data/spec/gsub_filter_spec.rb +87 -0
  57. data/spec/manifest_entry_spec.rb +46 -0
  58. data/spec/manifest_spec.rb +67 -0
  59. data/spec/matcher_spec.rb +141 -0
  60. data/spec/middleware_spec.rb +199 -0
  61. data/spec/ordering_concat_filter_spec.rb +42 -0
  62. data/spec/pipeline_spec.rb +232 -0
  63. data/spec/project_spec.rb +295 -0
  64. data/spec/rake_acceptance_spec.rb +738 -0
  65. data/spec/rake_tasks_spec.rb +21 -0
  66. data/spec/reject_matcher_spec.rb +31 -0
  67. data/spec/sorted_pipeline_spec.rb +27 -0
  68. data/spec/spec_helper.rb +38 -0
  69. data/spec/support/spec_helpers/file_utils.rb +35 -0
  70. data/spec/support/spec_helpers/filters.rb +37 -0
  71. data/spec/support/spec_helpers/input_helpers.rb +23 -0
  72. data/spec/support/spec_helpers/memory_file_wrapper.rb +31 -0
  73. data/spec/support/spec_helpers/memory_manifest.rb +19 -0
  74. data/tools/perfs +101 -0
  75. metadata +215 -0
@@ -0,0 +1,17 @@
1
+ module Rake
2
+ class Pipeline
3
+ # The general Rake::Pipeline error class
4
+ class Error < ::StandardError
5
+ end
6
+
7
+ # The error that Rake::Pipeline uses when it detects
8
+ # that a file uses an improper encoding.
9
+ class EncodingError < Error
10
+ end
11
+
12
+ # The error that Rake::Pipeline uses if you try to
13
+ # write to a FileWrapper before creating it.
14
+ class UnopenedFile < Error
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,182 @@
1
+ module Rake
2
+ class Pipeline
3
+ # This class wraps a file for consumption inside of filters. It is
4
+ # initialized with a root and path, and filters usually use the
5
+ # {#read} and {#write} methods to work with these files.
6
+ #
7
+ # The {#root} and +path+ parameters are provided by the {Filter}
8
+ # class' internal implementation. Individual filters do not need
9
+ # to worry about them.
10
+ #
11
+ # The root of a {FileWrapper} is always an absolute path.
12
+ class FileWrapper
13
+ # @return [String] an absolute path representing this {FileWrapper}'s
14
+ # root directory.
15
+ attr_accessor :root
16
+
17
+ # @return [String] the path to the file represented by the {FileWrapper},
18
+ # relative to its {#root}.
19
+ attr_accessor :path
20
+
21
+ # @return [String] the encoding that the file represented by this
22
+ # {FileWrapper} is encoded in. Filters set the {#encoding} to
23
+ # +BINARY+ if they are declared as processing binary data.
24
+ attr_accessor :encoding
25
+
26
+ # Create a new {FileWrapper}, passing in optional root, path, and
27
+ # encoding. Any of the parameters can be ommitted and supplied later.
28
+ #
29
+ # @return [void]
30
+ def initialize(root=nil, path=nil, encoding="UTF-8")
31
+ @root, @path, @encoding = root, path, encoding
32
+ @created_file = nil
33
+ end
34
+
35
+ # Create a new {FileWrapper FileWrapper} with the same root and
36
+ # path as this {FileWrapper FileWrapper}, but with a specified
37
+ # encoding.
38
+ #
39
+ # @param [String] encoding the encoding for the new object
40
+ # @return [FileWrapper]
41
+ def with_encoding(encoding)
42
+ self.class.new(@root, @path, encoding)
43
+ end
44
+
45
+ # A {FileWrapper} is equal to another {FileWrapper} for hashing purposes
46
+ # if they have the same {#root} and {#path}
47
+ #
48
+ # @param [FileWrapper] other another {FileWrapper} to compare.
49
+ # @return [true,false]
50
+ def eql?(other)
51
+ return false unless other.is_a?(self.class)
52
+ root == other.root && path == other.path
53
+ end
54
+ alias == eql?
55
+
56
+ # Similar to {#eql?}, generate a {FileWrapper}'s {#hash} from its {#root}
57
+ # and {#path}
58
+ #
59
+ # @see #eql?
60
+ # @return [Fixnum] a hash code
61
+ def hash
62
+ [root, path].hash
63
+ end
64
+
65
+ # The full path of a FileWrapper is its root joined with its path
66
+ #
67
+ # @return [String] the {FileWrapper}'s full path
68
+ def fullpath
69
+ raise "#{root}, #{path}" unless root =~ /^(\/|[a-zA-Z]:[\\\/])/
70
+ File.join(root, path)
71
+ end
72
+
73
+ # Check to see if this file is inside the given directory
74
+ #
75
+ # @return [Boolean]
76
+ def in_directory?(directory)
77
+ !!(fullpath =~ %r{^#{Regexp.escape(directory)}/})
78
+ end
79
+
80
+ # Make FileWrappers sortable
81
+ #
82
+ # @param [FileWrapper] other {FileWrapper FileWrapper}
83
+ # @return [Fixnum] -1, 0, or 1
84
+ def <=>(other)
85
+ [root, path, encoding] <=> [other.root, other.path, other.encoding]
86
+ end
87
+
88
+ # Does the file represented by the {FileWrapper} exist in the file system?
89
+ #
90
+ # @return [true,false]
91
+ def exists?
92
+ File.exists?(fullpath)
93
+ end
94
+
95
+ # Read the contents of the file represented by the {FileWrapper}.
96
+ #
97
+ # Read the file using the {FileWrapper}'s encoding, which will result in
98
+ # this method returning a +String+ tagged with the {FileWrapper}'s encoding.
99
+ #
100
+ # @return [String] the contents of the file
101
+ # @raise [EncodingError] when the contents of the file are not valid in the
102
+ # expected encoding specified in {#encoding}.
103
+ def read
104
+ contents = if "".respond_to?(:encode)
105
+ File.read(fullpath, :encoding => encoding)
106
+ else
107
+ File.read(fullpath)
108
+ end
109
+
110
+ # In our unit tests Rubinius returns false when the encoding is BINARY
111
+ # The encoding type check bypasses the problem and is probably acceptable, but isn't ideal
112
+ if encoding != "BINARY" && "".respond_to?(:encode) && !contents.valid_encoding?
113
+ raise EncodingError, "The file at the path #{fullpath} is not valid #{encoding}. Please save it again as #{encoding}."
114
+ end
115
+
116
+ contents
117
+ end
118
+
119
+ # Create a new file at the {FileWrapper}'s {#fullpath}. If the file already
120
+ # exists, it will be overwritten.
121
+ #
122
+ # @api private
123
+ # @yieldparam [File] file the newly created file
124
+ # @return [File] if a block was not given
125
+ def create
126
+ FileUtils.mkdir_p(File.dirname(fullpath))
127
+
128
+ @created_file = if "".respond_to?(:encode)
129
+ File.open(fullpath, "w:#{encoding}")
130
+ else
131
+ File.open(fullpath, "w")
132
+ end
133
+
134
+ if block_given?
135
+ yield @created_file
136
+ end
137
+
138
+ @created_file
139
+ ensure
140
+ if block_given?
141
+ @created_file.close
142
+ @created_file = nil
143
+ end
144
+ end
145
+
146
+ # Close the file represented by the {FileWrapper} if it was previously opened.
147
+ #
148
+ # @api private
149
+ # @return [void]
150
+ def close
151
+ raise IOError, "closed stream" unless @created_file
152
+ @created_file.close
153
+ @created_file = nil
154
+ end
155
+
156
+ # Check to see whether the file represented by the {FileWrapper} is open.
157
+ #
158
+ # @api private
159
+ # @return [true,false]
160
+ def closed?
161
+ @created_file.nil?
162
+ end
163
+
164
+ # Write a String to a previously opened file. This method is called repeatedly
165
+ # by a {Filter}'s +#generate_output+ method and does not create a brand new
166
+ # file for each invocation.
167
+ #
168
+ # @raise [UnopenedFile] if the file is not already opened.
169
+ def write(string)
170
+ raise UnopenedFile unless @created_file
171
+ @created_file.write(string)
172
+ end
173
+
174
+ # @return [String] A pretty representation of the {FileWrapper}.
175
+ def inspect
176
+ "#<FileWrapper root=#{root.inspect} path=#{path.inspect} encoding=#{encoding.inspect}>"
177
+ end
178
+
179
+ alias to_s inspect
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,249 @@
1
+ require "rake"
2
+
3
+ module Rake
4
+ class Pipeline
5
+
6
+ # A Filter is added to a pipeline and converts input files
7
+ # into output files.
8
+ #
9
+ # Filters operate on FileWrappers, which abstract away the
10
+ # root directory of a file, providing a relative path and
11
+ # a mechanism for reading and writing.
12
+ #
13
+ #
14
+ # For instance, a filter to wrap the contents of each file
15
+ # in a JavaScript closure would look like:
16
+ #
17
+ # !!!ruby
18
+ # require "json"
19
+ #
20
+ # class ClosureFilter < Rake::Pipeline::Filter
21
+ # def generate_output(inputs, output)
22
+ # inputs.each do |input|
23
+ # output.write "(function() { #{input.read.to_json} })()"
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # A filter's files come from the input directory or the directory
29
+ # owned by the previous filter, but filters are insulated from
30
+ # this concern.
31
+ #
32
+ # You can call +path+ on a FileWrapper to get the file's relative
33
+ # path, or `fullpath` to get its absolute path, but you should,
34
+ # in general, not use `fullpath` but instead use methods of
35
+ # FileWrapper like `read` and `write` that abstract the details
36
+ # from you.
37
+ #
38
+ # @see ConcatFilter Rake::Pipeline::ConcatFilter for another
39
+ # example filter implementation.
40
+ #
41
+ # @abstract
42
+ class Filter
43
+ # @return [Array<FileWrapper>] an Array of FileWrappers that
44
+ # represent the inputs of this filter. The Pipeline will
45
+ # usually set this up.
46
+ attr_accessor :input_files
47
+
48
+ # @return [Proc] a block that returns the relative output
49
+ # filename for a particular input file. If the block accepts
50
+ # just one argument, it will be passed the input's path. If
51
+ # it accepts two, it will also be passed the input itself.
52
+ attr_accessor :output_name_generator
53
+
54
+ # @return [String] the root directory to write output files
55
+ # to. For the last filter in a pipeline, the pipeline will
56
+ # set this to the pipeline's output. For all other filters,
57
+ # the pipeline will create a temporary directory that it
58
+ # also uses when creating FileWrappers for the next filter's
59
+ # inputs.
60
+ attr_accessor :output_root
61
+
62
+ # @return [Array<Rake::Task>] an Array of Rake tasks created
63
+ # for this filter. Each unique output file will get a
64
+ # single task.
65
+ attr_reader :rake_tasks
66
+
67
+ # @return [Rake::Application] the Rake::Application that the
68
+ # filter should define new rake tasks on.
69
+ attr_writer :rake_application
70
+
71
+ # @return [Rake::Pipeline] the Rake::Pipeline that contains
72
+ # this filter.
73
+ attr_accessor :pipeline
74
+
75
+ attr_writer :manifest, :last_manifest
76
+
77
+ attr_writer :file_wrapper_class
78
+
79
+ # @param [Proc] block a block to use as the Filter's
80
+ # {#output_name_generator}.
81
+ def initialize(&block)
82
+ block ||= proc { |input| input }
83
+ @output_name_generator = block
84
+ @input_files = []
85
+ end
86
+
87
+ # @return [Rake::Pipeline::Manifest] the manifest passed
88
+ # to generated rake tasks. Use the pipeline's manifest
89
+ # if this is not set
90
+ def manifest
91
+ @manifest || pipeline.manifest
92
+ end
93
+
94
+ def last_manifest
95
+ @last_manifest || pipeline.last_manifest
96
+ end
97
+
98
+ # Invoke this method in a subclass of Filter to declare that
99
+ # it expects to work with BINARY data, and that data that is
100
+ # not valid UTF-8 should be allowed.
101
+ #
102
+ # @return [void]
103
+ def self.processes_binary_files
104
+ define_method(:encoding) { "BINARY" }
105
+ end
106
+
107
+ # @return [Class] the class to use as the wrapper for output
108
+ # files.
109
+ def file_wrapper_class
110
+ @file_wrapper_class ||= FileWrapper
111
+ end
112
+
113
+ # Set the input files to a list of FileWrappers. The filter
114
+ # will map these into equivalent FileWrappers with the
115
+ # filter's encoding applied.
116
+ #
117
+ # By default, a filter's encoding is +UTF-8+, unless
118
+ # it calls #processes_binary_files, which changes it to
119
+ # +BINARY+.
120
+ #
121
+ # @param [Array<FileWrapper>] a list of FileWrapper objects
122
+ def input_files=(files)
123
+ @input_files = files.map do |file|
124
+ file.with_encoding(encoding)
125
+ end
126
+ end
127
+
128
+ # A hash of output files pointing at their associated input
129
+ # files. The output names are created by applying the
130
+ # {#output_name_generator} to each input file.
131
+ #
132
+ # For exmaple, if you had the following input files:
133
+ #
134
+ # javascripts/jquery.js
135
+ # javascripts/sproutcore.js
136
+ # stylesheets/sproutcore.css
137
+ #
138
+ # And you had the following {#output_name_generator}:
139
+ #
140
+ # !!!ruby
141
+ # filter.output_name_generator = proc do |filename|
142
+ # # javascripts/jquery.js becomes:
143
+ # # ["javascripts", "jquery", "js"]
144
+ # directory, file, ext = file.split(/[\.\/]/)
145
+ #
146
+ # "#{directory}.#{ext}"
147
+ # end
148
+ #
149
+ # You would end up with the following hash:
150
+ #
151
+ # !!!ruby
152
+ # {
153
+ # #<FileWrapper path="javascripts.js" root="#{output_root}> => [
154
+ # #<FileWrapper path="javascripts/jquery.js" root="#{previous_filter.output_root}">,
155
+ # #<FileWrapper path="javascripts/sproutcore.js" root="#{previous_filter.output_root}">
156
+ # ],
157
+ # #<FileWrapper path="stylesheets.css" root="#{output_root}"> => [
158
+ # #<FileWrapper path="stylesheets/sproutcore.css" root=#{previous_filter.output_root}">
159
+ # ]
160
+ # }
161
+ #
162
+ # Each output file becomes a Rake task, which invokes the +#generate_output+
163
+ # method defined by the subclass of {Filter} with the Array of inputs and
164
+ # the output (all as {FileWrapper}s).
165
+ #
166
+ # @return [Hash{FileWrapper => Array<FileWrapper>}]
167
+ def outputs
168
+ hash = {}
169
+
170
+ input_files.each do |file|
171
+ output_wrappers(file).each do |output|
172
+ hash[output] ||= []
173
+ hash[output] << file
174
+ end
175
+ end
176
+
177
+ hash
178
+ end
179
+
180
+ # An Array of the {FileWrapper} objects that rerepresent this filter's
181
+ # output files. It is the same as +outputs.keys+.
182
+ #
183
+ # @see #outputs
184
+ # @return [Array<FileWrapper>]
185
+ def output_files
186
+ input_files.map { |file| output_wrappers(file) }.flatten.uniq
187
+ end
188
+
189
+ # The Rake::Application that the filter should define new tasks on.
190
+ #
191
+ # @return [Rake::Application]
192
+ def rake_application
193
+ @rake_application || Rake.application
194
+ end
195
+
196
+ # @param [FileWrapper] file wrapper to get paths for
197
+ # @return [Array<String>] array of file paths within additional dependencies
198
+ def additional_dependencies(input)
199
+ []
200
+ end
201
+
202
+ # Generate the Rake tasks for the output files of this filter.
203
+ #
204
+ # @see #outputs #outputs (for information on how the output files are determined)
205
+ # @return [void]
206
+ def generate_rake_tasks
207
+ @rake_tasks = outputs.map do |output, inputs|
208
+ additional_paths = []
209
+ inputs.each do |input|
210
+
211
+ create_file_task(input.fullpath).dynamic do
212
+ additional_paths += additional_dependencies(input)
213
+ end
214
+ end
215
+ additional_paths.each { |path| create_file_task(path) }
216
+
217
+ create_file_task(output.fullpath, inputs.map(&:fullpath)) do
218
+ output.create { generate_output(inputs, output) }
219
+ end
220
+ end
221
+ end
222
+
223
+ private
224
+ # @attr_reader
225
+ def encoding
226
+ "UTF-8"
227
+ end
228
+
229
+ def create_file_task(output, deps=[], &block)
230
+ task = rake_application.define_task(Rake::Pipeline::DynamicFileTask, output => deps, &block)
231
+ task.manifest = manifest
232
+ task.last_manifest = last_manifest
233
+ task
234
+ end
235
+
236
+ def output_wrappers(input)
237
+ output_paths(input).map do |path|
238
+ file_wrapper_class.new(output_root, path, encoding)
239
+ end
240
+ end
241
+
242
+ def output_paths(input)
243
+ args = [ input.path ]
244
+ args << input if output_name_generator.arity == 2
245
+ Array(output_name_generator.call(*args))
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,4 @@
1
+ require "rake-pipeline/filters/concat_filter"
2
+ require "rake-pipeline/filters/ordering_concat_filter"
3
+ require "rake-pipeline/filters/pipeline_finalizing_filter"
4
+ require "rake-pipeline/filters/gsub_filter"