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,34 @@
1
+ module Rake
2
+ class Pipeline
3
+ # Represents a single entry in a dynamic dependency {Manifest}.
4
+ class ManifestEntry
5
+ # Create a new entry from the given hash.
6
+ def self.from_hash(hash)
7
+ entry = new
8
+
9
+ entry.mtime = hash["mtime"]
10
+
11
+ hash["deps"].each do |dep, time_string|
12
+ entry.deps[dep] = time_string
13
+ end
14
+
15
+ entry
16
+ end
17
+
18
+ attr_accessor :deps, :mtime
19
+
20
+ def initialize(deps={}, mtime=nil)
21
+ @deps, @mtime = deps, mtime
22
+ end
23
+
24
+ def as_json
25
+ { :deps => @deps, :mtime => @mtime }
26
+ end
27
+
28
+ def ==(other)
29
+ mtime == other.mtime
30
+ deps == other.deps
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,141 @@
1
+ require "strscan"
2
+
3
+ module Rake
4
+ class Pipeline
5
+ # A Matcher is a type of pipeline that restricts its
6
+ # filters to a particular pattern.
7
+ #
8
+ # A Matcher's pattern is a File glob.
9
+ #
10
+ # For instance, to restrict filters to operating on
11
+ # JavaScript files in the +app+ directory, the Matcher's
12
+ # {Pipeline#inputs inputs} should include +"app"+,
13
+ # and its glob would be +"*.js"+.
14
+ #
15
+ # In general, you should not use Matcher directly. Instead use
16
+ # {DSL#match} in the block passed to {Pipeline.build}.
17
+ class Matcher < Pipeline
18
+ attr_reader :glob
19
+
20
+ # @return [Rake::Pipeline] the Rake::Pipeline that contains
21
+ # this matcher.
22
+ attr_accessor :pipeline
23
+
24
+ # A glob matcher that a filter's input files must match
25
+ # in order to be processed by the filter.
26
+ #
27
+ # @return [String]
28
+ def glob=(pattern)
29
+ @glob = pattern
30
+ if pattern.kind_of?(Regexp)
31
+ @pattern = pattern
32
+ else
33
+ @pattern = scan_string
34
+ end
35
+ end
36
+
37
+ # A list of the output files that invoking this pipeline will
38
+ # generate. This will include the outputs of files matching
39
+ # the {#glob glob} and any inputs that did not match the
40
+ # glob.
41
+ #
42
+ # This will make those inputs available to any additional
43
+ # filters or matchers.
44
+ #
45
+ # @return [Array<FileWrapper>]
46
+ def output_files
47
+ super + input_files.reject do |file|
48
+ file.path =~ @pattern
49
+ end
50
+ end
51
+
52
+ # Override {Pipeline#finalize} to do nothing. We want to pass
53
+ # on our unmatched inputs to the next part of the pipeline.
54
+ #
55
+ # @return [void]
56
+ # @api private
57
+ def finalize
58
+ end
59
+
60
+ protected
61
+ # Let our containing pipeline generate temp directories for us.
62
+ def generate_tmpdir
63
+ pipeline.generate_tmpdir
64
+ end
65
+
66
+ private
67
+ # Override the default {Pipeline#eligible_input_files}
68
+ # to include only files that match the {#glob glob}.
69
+ #
70
+ # @return [Array<FileWrapper>]
71
+ def eligible_input_files
72
+ input_files.select do |file|
73
+ file.path =~ @pattern
74
+ end
75
+ end
76
+
77
+ # Convert string to regexp using StringScanner
78
+ #
79
+ # @return [Regexp]
80
+ def scan_string
81
+ scanner = StringScanner.new(glob)
82
+
83
+ output, pos = "", 0
84
+
85
+ # keep scanning until end of String
86
+ until scanner.eos?
87
+
88
+ # look for **/, *, {...}, or the end of the string
89
+ new_chars = scanner.scan_until %r{
90
+ \*\*/
91
+ | /\*\*/
92
+ | \*
93
+ | \{[^\}]*\}
94
+ | $
95
+ }x
96
+
97
+ # get the new part of the string up to the match
98
+ before = new_chars[0, new_chars.size - scanner.matched_size]
99
+
100
+ # get the match and new position
101
+ match = scanner.matched
102
+ pos = scanner.pos
103
+
104
+ # add any literal characters to the output
105
+ output << Regexp.escape(before) if before
106
+
107
+ output << case match
108
+ when "/**/"
109
+ # /**/ matches either a "/" followed by any number
110
+ # of characters or a single "/"
111
+ "(/.*|/)"
112
+ when "**/"
113
+ # **/ matches the beginning of the path or
114
+ # any number of characters followed by a "/"
115
+ "(^|.*/)"
116
+ when "*"
117
+ # * matches any number of non-"/" characters
118
+ "[^/]*"
119
+ when /\{.*\}/
120
+ # {...} is split over "," and glued back together
121
+ # as an or condition
122
+ "(" + match[1...-1].gsub(",", "|") + ")"
123
+ else String
124
+ # otherwise, we've grabbed until the end
125
+ match
126
+ end
127
+ end
128
+
129
+ if glob.include?("/")
130
+ # if the pattern includes a /, it must match the
131
+ # entire input, not just the end.
132
+ Regexp.new("^#{output}$", "i")
133
+ else
134
+ # anchor the pattern either at the beginning of the
135
+ # path or at any "/" character
136
+ Regexp.new("(^|/)#{output}$", "i")
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,72 @@
1
+ require "rack"
2
+
3
+ module Rake
4
+ class Pipeline
5
+ # This middleware is used to provide a server that will continuously
6
+ # compile your files on demand.
7
+ #
8
+ # @example
9
+ # !!!ruby
10
+ # use Rake::Pipeline::Middleware, Rake::Pipeline.build {
11
+ # input "app/assets"
12
+ # output "public"
13
+ #
14
+ # ...
15
+ # }
16
+ class Middleware
17
+ attr_accessor :project
18
+
19
+ # @param [#call] app a Rack application
20
+ # @param [Rake::Pipeline::Project] an existing project
21
+ def initialize(app, project)
22
+ @app = app
23
+ @project = project
24
+ end
25
+
26
+ # Automatically compiles your assets if required and
27
+ # serves them up.
28
+ #
29
+ # @param [Hash] env a Rack environment
30
+ # @return [Array(Fixnum, Hash, #each)] A rack response
31
+ def call(env)
32
+ project.invoke
33
+ path = env["PATH_INFO"]
34
+
35
+ if project.maps.has_key?(path)
36
+ return project.maps[path].call(env)
37
+ end
38
+
39
+ if filename = file_for(path)
40
+ if File.directory?(filename)
41
+ index = File.join(filename, "index.html")
42
+ filename = File.file?(index) ? index : nil
43
+ end
44
+
45
+ if filename
46
+ return response_for(filename)
47
+ end
48
+ end
49
+
50
+ @app.call(env)
51
+ end
52
+
53
+ private
54
+ def response_for(file)
55
+ [ 200, headers_for(file), File.open(file, "r") ]
56
+ end
57
+
58
+ def file_for(path)
59
+ project.pipelines.each do |pipeline|
60
+ file = Dir[File.join(pipeline.output_root, path)].sort.first
61
+ return file unless file.nil?
62
+ end
63
+ nil
64
+ end
65
+
66
+ def headers_for(path)
67
+ mime = Rack::Mime.mime_type(File.extname(path), "text/plain")
68
+ { "Content-Type" => mime }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,8 @@
1
+ namespace :assets do
2
+ desc "Precompile assets using Rake::Pipeline"
3
+ task :precompile do
4
+ config = Rails.application.config.rake_pipeline_assetfile
5
+ Rake::Pipeline::Project.new(config).invoke
6
+ end
7
+ end
8
+
@@ -0,0 +1,335 @@
1
+ require "digest"
2
+
3
+ module Rake
4
+ class Pipeline
5
+ # A Project controls the lifecycle of a series of Pipelines,
6
+ # creating them from an Assetfile and recreating them if the
7
+ # Assetfile changes.
8
+ class Project
9
+ # @return [Pipeline] the list of pipelines in the project
10
+ attr_reader :pipelines
11
+
12
+ attr_reader :maps
13
+
14
+ # @return [String|nil] the path to the project's Assetfile
15
+ # or nil if it was created without an Assetfile.
16
+ attr_reader :assetfile_path
17
+
18
+ # @return [String|nil] the digest of the Assetfile the
19
+ # project was created with, or nil if the project
20
+ # was created without an Assetfile.
21
+ attr_reader :assetfile_digest
22
+
23
+ # @return [String] the directory path for temporary files
24
+ attr_reader :tmpdir
25
+
26
+ # @return [String] the directory path where pipelines will
27
+ # write their outputs by default
28
+ attr_reader :default_output_root
29
+
30
+ # @return [Array] a list of filters to be applied before
31
+ # the specified filters in every pipeline
32
+ attr_writer :before_filters
33
+
34
+ # @return [Array] a list of filters to be applied after
35
+ # the specified filters in every pipeline
36
+ attr_writer :after_filters
37
+
38
+ class << self
39
+ # Configure a new project by evaluating a block with the
40
+ # Rake::Pipeline::DSL::ProjectDSL class.
41
+ #
42
+ # @see Rake::Pipeline::Filter Rake::Pipeline::Filter
43
+ #
44
+ # @example
45
+ # Rake::Pipeline::Project.build do
46
+ # tmpdir "tmp"
47
+ # output "public"
48
+ #
49
+ # input "app/assets" do
50
+ # concat "app.js"
51
+ # end
52
+ # end
53
+ #
54
+ # @return [Rake::Pipeline::Project] the newly configured project
55
+ def build(&block)
56
+ project = new
57
+ project.build(&block)
58
+ end
59
+
60
+ # @return [Array[String]] an array of strings that will be
61
+ # appended to {#digested_tmpdir}.
62
+ def digest_additions
63
+ @digest_additions ||= []
64
+ end
65
+
66
+ # Set {.digest_additions} to a sorted copy of the given array.
67
+ def digest_additions=(additions)
68
+ @digest_additions = additions.sort
69
+ end
70
+
71
+ # Add a value to the list of strings to append to the digest
72
+ # temp directory. Libraries can use this to add (for example)
73
+ # their version numbers so that the pipeline will be rebuilt
74
+ # if the library version changes.
75
+ #
76
+ # @example
77
+ # Rake::Pipeline::Project.add_to_digest(Rake::Pipeline::Web::Filters::VERSION)
78
+ #
79
+ # @param [#to_s] str a value to append to {#digested_tmpdir}.
80
+ def add_to_digest(str)
81
+ self.digest_additions << str.to_s
82
+ self.digest_additions.sort!
83
+ end
84
+ end
85
+
86
+ # @param [String|Pipeline] assetfile_or_pipeline
87
+ # if this a String, create a Pipeline from the Assetfile at
88
+ # that path. If it's a Pipeline, just wrap that pipeline.
89
+ def initialize(assetfile_or_pipeline=nil)
90
+ reset!
91
+ if assetfile_or_pipeline.kind_of?(String)
92
+ @assetfile_path = File.expand_path(assetfile_or_pipeline)
93
+ rebuild_from_assetfile(@assetfile_path)
94
+ elsif assetfile_or_pipeline
95
+ @pipelines << assetfile_or_pipeline
96
+ end
97
+ end
98
+
99
+ # Evaluate a block using the Rake::Pipeline::DSL::ProjectDSL
100
+ # DSL against an existing project.
101
+ def build(&block)
102
+ DSL::ProjectDSL.evaluate(self, &block) if block
103
+ self
104
+ end
105
+
106
+ # Invoke all of the project's pipelines, detecting any changes
107
+ # to the Assetfile and rebuilding the pipelines if necessary.
108
+ #
109
+ # @return [void]
110
+ # @see Rake::Pipeline#invoke
111
+ def invoke
112
+ @invoke_mutex.synchronize do
113
+ last_manifest.read_manifest
114
+
115
+ if dirty?
116
+ rebuild_from_assetfile(assetfile_path) if assetfile_dirty?
117
+
118
+ # The temporary files have to be cleaned otherwise
119
+ # there will be a "ghost" input. Here's an example
120
+ # rake task: application.js => [a.js, b.js]. Deleting a.js
121
+ # will make application.js => [b.js]. The task correctly checks
122
+ # if B has changed (which it hasn't) and says that application.js
123
+ # is correct. Cleaning tmp files ensures that this doesn't happen.
124
+ clean if files_deleted?
125
+
126
+ pipelines.each(&:invoke)
127
+
128
+ manifest.write_manifest
129
+ end
130
+ end
131
+ end
132
+
133
+ # Remove the project's temporary and output files.
134
+ def clean
135
+ last_manifest.clear
136
+ files_to_clean.each { |file| FileUtils.rm_rf(file) }
137
+ end
138
+
139
+ # Clean out old tmp directories from the pipeline's
140
+ # {Rake::Pipeline#tmpdir}.
141
+ #
142
+ # @return [void]
143
+ def cleanup_tmpdir
144
+ obsolete_tmpdirs.each { |dir| FileUtils.rm_rf(dir) }
145
+ end
146
+
147
+ # Set the default output root of this project and expand its path.
148
+ #
149
+ # @param [String] root this pipeline's output root
150
+ def default_output_root=(root)
151
+ @default_output_root = File.expand_path(root)
152
+ end
153
+
154
+ # Set the temporary directory for this project and expand its path.
155
+ #
156
+ # @param [String] root this project's temporary directory
157
+ def tmpdir=(dir)
158
+ @tmpdir = File.expand_path(dir)
159
+ end
160
+
161
+ # @return [String] A subdirectory of {#tmpdir} with the digest of
162
+ # the Assetfile's contents and any {.digest_additions} in its
163
+ # name.
164
+ def digested_tmpdir
165
+ suffix = assetfile_digest
166
+ unless self.class.digest_additions.empty?
167
+ suffix += "-#{self.class.digest_additions.join('-')}"
168
+ end
169
+ File.join(tmpdir, "rake-pipeline-#{suffix}")
170
+ end
171
+
172
+ # @return Array[String] a list of the paths to temporary directories
173
+ # that don't match the pipline's Assetfile digest.
174
+ def obsolete_tmpdirs
175
+ if File.directory?(tmpdir)
176
+ Dir["#{tmpdir}/rake-pipeline-*"].sort.reject do |dir|
177
+ dir == digested_tmpdir
178
+ end
179
+ else
180
+ []
181
+ end
182
+ end
183
+
184
+ # @return Array[String] a list of files to delete to completely clean
185
+ # out a project's temporary and output files.
186
+ def files_to_clean
187
+ setup_pipelines
188
+ obsolete_tmpdirs + [digested_tmpdir] + output_files.map(&:fullpath)
189
+ end
190
+
191
+ # @return [Array[FileWrapper]] a list of the files that
192
+ # will be generated when this project is invoked.
193
+ def output_files
194
+ setup_pipelines
195
+ pipelines.map(&:output_files).flatten
196
+ end
197
+
198
+ # Build a new pipeline and add it to our list of pipelines.
199
+ def build_pipeline(input, glob=nil, &block)
200
+ pipeline = Rake::Pipeline.build({
201
+ :before_filters => @before_filters,
202
+ :after_filters => @after_filters,
203
+ :output_root => default_output_root,
204
+ :tmpdir => digested_tmpdir,
205
+ :project => self
206
+ }, &block)
207
+
208
+ if input.kind_of?(Array)
209
+ input.each { |x| pipeline.add_input(x) }
210
+ elsif input.kind_of?(Hash)
211
+ pipeline.inputs = input
212
+ else
213
+ pipeline.add_input(input, glob)
214
+ end
215
+
216
+ @pipelines << pipeline
217
+ pipeline
218
+ end
219
+
220
+ # @return [Manifest] the manifest to write dependency information
221
+ # to
222
+ def manifest
223
+ @manifest ||= Rake::Pipeline::Manifest.new(manifest_path)
224
+ end
225
+
226
+ # @return [Manifest] the manifest to write dependency information
227
+ # to
228
+ def last_manifest
229
+ @last_manifest ||= Rake::Pipeline::Manifest.new(manifest_path)
230
+ end
231
+
232
+ # @return [String] the path to the dynamic dependency manifest
233
+ def manifest_path
234
+ File.join(digested_tmpdir, "manifest.json")
235
+ end
236
+
237
+ private
238
+ # Reset this project's internal state to the default values.
239
+ #
240
+ # @return [void]
241
+ def reset!
242
+ @pipelines = []
243
+ @maps = {}
244
+ @tmpdir = "tmp"
245
+ @invoke_mutex = Mutex.new
246
+ @default_output_root = @assetfile_digest = @assetfile_path = nil
247
+ @manifest = @last_manifest = nil
248
+ end
249
+
250
+ # Reconfigure this project based on the Assetfile at path.
251
+ #
252
+ # @param [String] path the path to the Assetfile
253
+ # to use to configure the project.
254
+ # @param [String] source if given, this string is
255
+ # evaluated instead of reading the file at assetfile_path.
256
+ #
257
+ # @return [void]
258
+ def rebuild_from_assetfile(path, source=nil)
259
+ reset!
260
+ source ||= File.read(path)
261
+ @assetfile_digest = digest(source)
262
+ @assetfile_path = path
263
+ build { instance_eval(source, path, 1) }
264
+ end
265
+
266
+ # Setup the pipeline so its output files will be up to date.
267
+ def setup_pipelines
268
+ pipelines.map(&:setup_filters)
269
+ end
270
+
271
+ # @return [String] the SHA1 digest of the given string.
272
+ def digest(str)
273
+ Digest::SHA1.hexdigest(str)
274
+ end
275
+
276
+ def dirty?
277
+ assetfile_dirty? || files_deleted? || files_dirty?
278
+ end
279
+
280
+ def assetfile_dirty?
281
+ if assetfile_path
282
+ source = File.read(assetfile_path)
283
+ digest(source) != assetfile_digest
284
+ else
285
+ false
286
+ end
287
+ end
288
+
289
+ # Returns true if any of these conditions are met:
290
+ # * The pipeline hasn't been invoked yet
291
+ # * The input files have changed
292
+ # * There is a new input file
293
+ def files_dirty?
294
+ return true if manifest.empty?
295
+
296
+ previous_files = manifest.files
297
+
298
+ # check for modifications to new files
299
+ input_files.each do |input_file|
300
+ if !previous_files[input_file]
301
+ return true # there is a new file in the pipeline
302
+ elsif File.mtime(input_file).to_i != previous_files[input_file]
303
+ return true # existing file has been changed
304
+ end
305
+ end
306
+
307
+ false
308
+ end
309
+
310
+ def files_deleted?
311
+ last_manifest.files.each_key do |input_file|
312
+ return true if !File.exists?(input_file)
313
+ end
314
+
315
+ false
316
+ end
317
+
318
+ def input_files
319
+ static_input_files = pipelines.collect do |p|
320
+ p.input_files.reject { |file| file.in_directory? tmpdir }.map(&:fullpath)
321
+ end.flatten
322
+
323
+ dynamic_input_files = static_input_files.collect do |file|
324
+ if manifest[file]
325
+ manifest[file].deps.keys
326
+ else
327
+ nil
328
+ end
329
+ end.flatten.compact
330
+
331
+ static_input_files + dynamic_input_files
332
+ end
333
+ end
334
+ end
335
+ end