rake-pipeline 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.yardopts +2 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.markdown +4 -0
- data/README.yard +149 -0
- data/Rakefile +12 -0
- data/bin/rakep +27 -0
- data/lib/rake-pipeline.rb +365 -0
- data/lib/rake-pipeline/dsl.rb +146 -0
- data/lib/rake-pipeline/error.rb +17 -0
- data/lib/rake-pipeline/file_wrapper.rb +173 -0
- data/lib/rake-pipeline/filter.rb +209 -0
- data/lib/rake-pipeline/filters.rb +1 -0
- data/lib/rake-pipeline/filters/concat.rb +63 -0
- data/lib/rake-pipeline/matcher.rb +106 -0
- data/lib/rake-pipeline/middleware.rb +70 -0
- data/lib/rake-pipeline/rails_plugin.rb +8 -0
- data/lib/rake-pipeline/railtie.rb +17 -0
- data/lib/rake-pipeline/version.rb +6 -0
- data/rails/init.rb +2 -0
- data/rake-pipeline.gemspec +22 -0
- data/spec/concat_filter_spec.rb +60 -0
- data/spec/dsl_spec.rb +86 -0
- data/spec/encoding_spec.rb +106 -0
- data/spec/file_wrapper_spec.rb +105 -0
- data/spec/filter_spec.rb +216 -0
- data/spec/matcher_spec.rb +105 -0
- data/spec/middleware_spec.rb +149 -0
- data/spec/pipeline_spec.rb +160 -0
- data/spec/rake_acceptance_spec.rb +240 -0
- data/spec/spec_helper.rb +74 -0
- metadata +123 -0
@@ -0,0 +1 @@
|
|
1
|
+
require "rake-pipeline/filters/concat"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rake
|
2
|
+
class Pipeline
|
3
|
+
# A built-in filter that simply accepts a series
|
4
|
+
# of inputs and concatenates them into output files
|
5
|
+
# based on the output file name generator.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# !!!ruby
|
9
|
+
# Pipeline.build do
|
10
|
+
# input "app/assets", "**/*.js"
|
11
|
+
# output "public"
|
12
|
+
#
|
13
|
+
# # create a concatenated output file for each
|
14
|
+
# # directory of inputs.
|
15
|
+
# filter(Rake::Pipeline::ConcatFilter) do |input|
|
16
|
+
# # input files will look something like:
|
17
|
+
# # javascripts/admin/main.js
|
18
|
+
# # javascripts/admin/app.js
|
19
|
+
# # javascripts/users/main.js
|
20
|
+
# #
|
21
|
+
# # and the outputs will look like:
|
22
|
+
# # javascripts/admin.js
|
23
|
+
# # javascripts/users.js
|
24
|
+
# directory = File.dirname(input)
|
25
|
+
# ext = File.extname(input)
|
26
|
+
#
|
27
|
+
# "#{directory}#{ext}"
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
class ConcatFilter < Rake::Pipeline::Filter
|
31
|
+
# @param [String] string the name of the output file to
|
32
|
+
# concatenate inputs to.
|
33
|
+
# @param [Proc] block a block to use as the Filter's
|
34
|
+
# {#output_name_generator}.
|
35
|
+
def initialize(string=nil, &block)
|
36
|
+
block = proc { string } if string
|
37
|
+
super(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @method encoding
|
41
|
+
# @return [String] the String +"BINARY"+
|
42
|
+
processes_binary_files
|
43
|
+
|
44
|
+
# implement the {#generate_output} method required by
|
45
|
+
# the {Filter} API. In this case, simply loop through
|
46
|
+
# the inputs and write their contents to the output.
|
47
|
+
#
|
48
|
+
# Recall that this method will be called once for each
|
49
|
+
# unique output file.
|
50
|
+
#
|
51
|
+
# @param [Array<FileWrapper>] inputs an Array of
|
52
|
+
# {FileWrapper} objects representing the inputs to
|
53
|
+
# this filter.
|
54
|
+
# @param [FileWrapper] a single {FileWrapper} object
|
55
|
+
# representing the output.
|
56
|
+
def generate_output(inputs, output)
|
57
|
+
inputs.each do |input|
|
58
|
+
output.write input.read
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,106 @@
|
|
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#input_root input_root} should be +"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
|
+
# A glob matcher that a filter's input files must match
|
21
|
+
# in order to be processed by the filter.
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def glob=(pattern)
|
25
|
+
@glob = pattern
|
26
|
+
scanner = StringScanner.new(pattern)
|
27
|
+
|
28
|
+
output, pos = "", 0
|
29
|
+
|
30
|
+
# keep scanning until end of String
|
31
|
+
until scanner.eos?
|
32
|
+
|
33
|
+
# look for **/, *, {...}, or the end of the string
|
34
|
+
new_chars = scanner.scan_until %r{
|
35
|
+
\*\*/
|
36
|
+
| /\*\*/
|
37
|
+
| \*
|
38
|
+
| \{[^\}]*\}
|
39
|
+
| $
|
40
|
+
}x
|
41
|
+
|
42
|
+
# get the new part of the string up to the match
|
43
|
+
before = new_chars[0, new_chars.size - scanner.matched_size]
|
44
|
+
|
45
|
+
# get the match and new position
|
46
|
+
match = scanner.matched
|
47
|
+
pos = scanner.pos
|
48
|
+
|
49
|
+
# add any literal characters to the output
|
50
|
+
output << Regexp.escape(before) if before
|
51
|
+
|
52
|
+
output << case match
|
53
|
+
when "/**/"
|
54
|
+
# /**/ matches either a "/" followed by any number
|
55
|
+
# of characters or a single "/"
|
56
|
+
"(/.*|/)"
|
57
|
+
when "**/"
|
58
|
+
# **/ matches the beginning of the path or
|
59
|
+
# any number of characters followed by a "/"
|
60
|
+
"(^|.*/)"
|
61
|
+
when "*"
|
62
|
+
# * matches any number of non-"/" characters
|
63
|
+
"[^/]*"
|
64
|
+
when /\{.*\}/
|
65
|
+
# {...} is split over "," and glued back together
|
66
|
+
# as an or condition
|
67
|
+
"(" + match[1...-1].gsub(",", "|") + ")"
|
68
|
+
else String
|
69
|
+
# otherwise, we've grabbed until the end
|
70
|
+
match
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# anchor the pattern either at the beginning of the
|
75
|
+
# path or at any "/" character
|
76
|
+
@pattern = Regexp.new("(^|/)#{output}$", "i")
|
77
|
+
end
|
78
|
+
|
79
|
+
# A list of the output files that invoking this pipeline will
|
80
|
+
# generate. This will include the outputs of files matching
|
81
|
+
# the {#glob glob} and any inputs that did not match the
|
82
|
+
# glob.
|
83
|
+
#
|
84
|
+
# This will make those inputs available to any additional
|
85
|
+
# filters or matchers.
|
86
|
+
#
|
87
|
+
# @return [Array<FileWrapper>]
|
88
|
+
def output_files
|
89
|
+
super + input_files.reject do |file|
|
90
|
+
file.path =~ @pattern
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
# Override the default {Pipeline#eligible_input_files}
|
96
|
+
# to include only files that match the {#glob glob}.
|
97
|
+
#
|
98
|
+
# @return [Array<FileWrapper>]
|
99
|
+
def eligible_input_files
|
100
|
+
input_files.select do |file|
|
101
|
+
file.path =~ @pattern
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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 :pipeline
|
18
|
+
|
19
|
+
# @param [#call] a Rack application
|
20
|
+
# @param [Pipeline] a Rake::Pipeline
|
21
|
+
def initialize(app, pipeline)
|
22
|
+
@app = app
|
23
|
+
|
24
|
+
if pipeline.is_a?(String)
|
25
|
+
pipeline_source = File.read(pipeline)
|
26
|
+
pipeline = Pipeline.class_eval "build do\n#{pipeline_source}\nend", pipeline, 1
|
27
|
+
end
|
28
|
+
|
29
|
+
@pipeline = pipeline
|
30
|
+
end
|
31
|
+
|
32
|
+
# Automatically compiles your assets if required and
|
33
|
+
# serves them up.
|
34
|
+
#
|
35
|
+
# @param [Hash] env a Rack environment
|
36
|
+
# @return [Array(Fixnum, Hash, #each)] A rack response
|
37
|
+
def call(env)
|
38
|
+
pipeline.invoke_clean
|
39
|
+
path = env["PATH_INFO"]
|
40
|
+
|
41
|
+
if filename = file_for(path)
|
42
|
+
if File.directory?(filename)
|
43
|
+
index = File.join(filename, "index.html")
|
44
|
+
filename = File.file?(index) ? index : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
if filename
|
48
|
+
return response_for(filename)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@app.call(env)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def response_for(file)
|
57
|
+
[ 200, headers_for(file), File.open(file, "r") ]
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_for(path)
|
61
|
+
Dir[File.join(pipeline.output_root, path)].first
|
62
|
+
end
|
63
|
+
|
64
|
+
def headers_for(path)
|
65
|
+
mime = Rack::Mime.mime_type(File.extname(path), "text/plain")
|
66
|
+
{ "Content-Type" => mime }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'rake-pipeline/middleware'
|
2
|
+
|
3
|
+
Rails.configuration.after_initialize do
|
4
|
+
if defined?(RAKEP_ENABLED) && RAKEP_ENABLED
|
5
|
+
assetfile = defined?(RAKEP_ASSETFILE) ? RAKEP_ASSETFILE : 'Assetfile'
|
6
|
+
Rails.configuration.middleware.use(Rake::Pipeline::Middleware, assetfile)
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rake-pipeline/middleware"
|
2
|
+
|
3
|
+
module Rake
|
4
|
+
class Pipeline
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
config.rake_pipeline_enabled = false
|
7
|
+
config.rake_pipeline_assetfile = 'Assetfile'
|
8
|
+
|
9
|
+
initializer "rake-pipeline.assetfile" do |app|
|
10
|
+
if config.rake_pipeline_enabled
|
11
|
+
assetfile = File.join(Rails.root, config.rake_pipeline_assetfile)
|
12
|
+
config.app_middleware.use(Rake::Pipeline::Middleware, assetfile)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/rake-pipeline/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Yehuda Katz", "Tom Dale"]
|
6
|
+
gem.email = ["wycats@gmail.com"]
|
7
|
+
gem.description = "Simple Asset Management"
|
8
|
+
gem.summary = "Simple Asset Management"
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "rake-pipeline"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Rake::Pipeline::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "rake", "~> 0.9.0"
|
19
|
+
|
20
|
+
gem.add_development_dependency "rspec"
|
21
|
+
gem.add_development_dependency "rack-test"
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
describe "ConcatFilter" do
|
2
|
+
class MemoryFileWrapper < Struct.new(:root, :path, :encoding, :body)
|
3
|
+
@@files = {}
|
4
|
+
|
5
|
+
def self.files
|
6
|
+
@@files
|
7
|
+
end
|
8
|
+
|
9
|
+
def with_encoding(new_encoding)
|
10
|
+
self.class.new(root, path, new_encoding, body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def fullpath
|
14
|
+
File.join(root, path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@@files[fullpath] = self
|
19
|
+
self.body = ""
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
|
23
|
+
alias read body
|
24
|
+
|
25
|
+
def write(contents)
|
26
|
+
self.body << contents
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:input_files) {
|
31
|
+
[
|
32
|
+
MemoryFileWrapper.new("/path/to/input", "javascripts/jquery.js", "UTF-8", "jQuery = {};"),
|
33
|
+
MemoryFileWrapper.new("/path/to/input", "javascripts/sproutcore.js", "UTF-8", "SC = {};")
|
34
|
+
]
|
35
|
+
}
|
36
|
+
|
37
|
+
it "generates output" do
|
38
|
+
filter = Rake::Pipeline::ConcatFilter.new { "application.js" }
|
39
|
+
filter.file_wrapper_class = MemoryFileWrapper
|
40
|
+
filter.output_root = "/path/to/output"
|
41
|
+
filter.input_files = input_files
|
42
|
+
|
43
|
+
filter.output_files.should == [MemoryFileWrapper.new("/path/to/output", "application.js", "BINARY")]
|
44
|
+
|
45
|
+
tasks = filter.generate_rake_tasks
|
46
|
+
tasks.each(&:invoke)
|
47
|
+
|
48
|
+
file = MemoryFileWrapper.files["/path/to/output/application.js"]
|
49
|
+
file.body.should == "jQuery = {};SC = {};"
|
50
|
+
file.encoding.should == "BINARY"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "accepts a string to use as the output file name" do
|
54
|
+
filter = Rake::Pipeline::ConcatFilter.new("app.js")
|
55
|
+
filter.file_wrapper_class = MemoryFileWrapper
|
56
|
+
filter.output_root = "/path/to/output"
|
57
|
+
filter.input_files = input_files
|
58
|
+
filter.output_files.should == [MemoryFileWrapper.new("/path/to/output", "app.js", "BINARY")]
|
59
|
+
end
|
60
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
describe "Rake::Pipeline::DSL" do
|
2
|
+
ConcatFilter = Rake::Pipeline::SpecHelpers::Filters::ConcatFilter
|
3
|
+
|
4
|
+
let(:pipeline) { Rake::Pipeline.new }
|
5
|
+
let(:dsl) { Rake::Pipeline::DSL.new(pipeline) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
pipeline.input_root = "."
|
9
|
+
end
|
10
|
+
|
11
|
+
it "accepts a pipeline in its constructor" do
|
12
|
+
dsl.pipeline.should == pipeline
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#input" do
|
16
|
+
it "configures the pipeline's input_root" do
|
17
|
+
dsl.input "/app"
|
18
|
+
pipeline.input_root.should == "/app"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "configures the pipeline's input_glob" do
|
22
|
+
dsl.input "/app", "*.js"
|
23
|
+
pipeline.input_glob.should == "*.js"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "defaults the pipeline's input_glob to **/*" do
|
27
|
+
dsl.input "/app"
|
28
|
+
pipeline.input_glob.should == "**/*"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#filter" do
|
33
|
+
|
34
|
+
it "adds a new instance of the filter class to the pipeline's filters" do
|
35
|
+
pipeline.filters.should be_empty
|
36
|
+
dsl.filter ConcatFilter
|
37
|
+
pipeline.filters.should_not be_empty
|
38
|
+
pipeline.filters.last.should be_kind_of(ConcatFilter)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "takes a block to configure the filter's output file names" do
|
42
|
+
generator = proc { |input| "main.js" }
|
43
|
+
dsl.filter(ConcatFilter, &generator)
|
44
|
+
pipeline.filters.last.output_name_generator.should == generator
|
45
|
+
end
|
46
|
+
|
47
|
+
it "passes any extra arguments to the filter's constructor" do
|
48
|
+
filter_class = Class.new(Rake::Pipeline::Filter) do
|
49
|
+
attr_reader :args
|
50
|
+
def initialize(*args)
|
51
|
+
@args = args
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
dsl.filter filter_class, "foo", "bar"
|
56
|
+
pipeline.filters.last.args.should == %w(foo bar)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#match" do
|
61
|
+
it "creates a Matcher for the given glob" do
|
62
|
+
matcher = dsl.match("*.glob") {}
|
63
|
+
matcher.should be_kind_of(Rake::Pipeline::Matcher)
|
64
|
+
matcher.glob.should == "*.glob"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "adds the new matcher to the pipeline's filters" do
|
68
|
+
matcher = dsl.match("*.glob") {}
|
69
|
+
pipeline.filters.last.should == matcher
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#output" do
|
74
|
+
it "configures the pipeline's output_root" do
|
75
|
+
dsl.output "/path/to/output"
|
76
|
+
pipeline.output_root.should == "/path/to/output"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#tmpdir" do
|
81
|
+
it "configures the pipeline's tmpdir" do
|
82
|
+
dsl.tmpdir "/temporary"
|
83
|
+
pipeline.tmpdir.should == "/temporary"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|