rake-pipeline 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.travis.yml +12 -0
  2. data/Gemfile +1 -0
  3. data/README.markdown +1 -1
  4. data/README.yard +61 -32
  5. data/Rakefile +9 -0
  6. data/bin/rakep +1 -24
  7. data/lib/generators/rake/pipeline/install/install_generator.rb +70 -0
  8. data/lib/rake-pipeline.rb +117 -53
  9. data/lib/rake-pipeline/cli.rb +56 -0
  10. data/lib/rake-pipeline/dsl.rb +3 -140
  11. data/lib/rake-pipeline/dsl/pipeline_dsl.rb +168 -0
  12. data/lib/rake-pipeline/dsl/project_dsl.rb +108 -0
  13. data/lib/rake-pipeline/dynamic_file_task.rb +188 -0
  14. data/lib/rake-pipeline/file_wrapper.rb +1 -1
  15. data/lib/rake-pipeline/filter.rb +45 -15
  16. data/lib/rake-pipeline/filters.rb +3 -1
  17. data/lib/rake-pipeline/filters/{concat.rb → concat_filter.rb} +0 -0
  18. data/lib/rake-pipeline/filters/ordering_concat_filter.rb +38 -0
  19. data/lib/rake-pipeline/filters/pipeline_finalizing_filter.rb +19 -0
  20. data/lib/rake-pipeline/graph.rb +178 -0
  21. data/lib/rake-pipeline/manifest.rb +63 -0
  22. data/lib/rake-pipeline/manifest_entry.rb +34 -0
  23. data/lib/rake-pipeline/matcher.rb +65 -30
  24. data/lib/rake-pipeline/middleware.rb +15 -12
  25. data/lib/rake-pipeline/precompile.rake +8 -0
  26. data/lib/rake-pipeline/project.rb +280 -0
  27. data/lib/rake-pipeline/railtie.rb +16 -1
  28. data/lib/rake-pipeline/server.rb +15 -0
  29. data/lib/rake-pipeline/version.rb +2 -2
  30. data/rake-pipeline.gemspec +2 -0
  31. data/spec/cli_spec.rb +71 -0
  32. data/spec/concat_filter_spec.rb +1 -27
  33. data/spec/{dsl_spec.rb → dsl/pipeline_dsl_spec.rb} +32 -18
  34. data/spec/dsl/project_dsl_spec.rb +41 -0
  35. data/spec/dynamic_file_task_spec.rb +111 -0
  36. data/spec/encoding_spec.rb +6 -8
  37. data/spec/file_wrapper_spec.rb +19 -2
  38. data/spec/filter_spec.rb +120 -22
  39. data/spec/graph_spec.rb +56 -0
  40. data/spec/manifest_entry_spec.rb +51 -0
  41. data/spec/manifest_spec.rb +67 -0
  42. data/spec/matcher_spec.rb +35 -2
  43. data/spec/middleware_spec.rb +123 -75
  44. data/spec/ordering_concat_filter_spec.rb +39 -0
  45. data/spec/pipeline_spec.rb +95 -34
  46. data/spec/project_spec.rb +293 -0
  47. data/spec/rake_acceptance_spec.rb +307 -67
  48. data/spec/rake_tasks_spec.rb +21 -0
  49. data/spec/spec_helper.rb +11 -48
  50. data/spec/support/spec_helpers/file_utils.rb +35 -0
  51. data/spec/support/spec_helpers/filters.rb +16 -0
  52. data/spec/support/spec_helpers/input_helpers.rb +23 -0
  53. data/spec/support/spec_helpers/memory_file_wrapper.rb +31 -0
  54. data/tools/perfs +107 -0
  55. metadata +100 -12
@@ -0,0 +1,12 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ree
6
+ - jruby
7
+ - rbx
8
+
9
+ notifications:
10
+ email:
11
+ - wycats@gmail.com
12
+ - dudley@steambone.org
data/Gemfile CHANGED
@@ -8,3 +8,4 @@ gem "flog"
8
8
  gem "simplecov", :require => false
9
9
  gem "yard"
10
10
  gem "rdiscount"
11
+ gem "pry"
@@ -1,4 +1,4 @@
1
1
  # Rake::Pipeline
2
2
 
3
3
  The canonical documentation for Rake::Pipeline is hosted at
4
- <a href="http://rubydoc.info/github/livingsocial/rake-pipeline/master/file/README.yard">rubydoc.info</a>
4
+ <a href="http://rubydoc.info/github/livingsocial/rake-pipeline/master/file/README.yard">rubydoc.info</a>.
@@ -6,45 +6,72 @@ output files based on input changes.
6
6
 
7
7
  = Usage
8
8
 
9
- The easiest way to use Rake::Pipeline is via a +Assetfile+ file in the
9
+ The easiest way to use Rake::Pipeline is via an +Assetfile+ file in the
10
10
  root of your project.
11
11
 
12
12
  A sample +Assetfile+ looks like this:
13
13
 
14
14
  !!!ruby
15
- input "assets"
16
15
  output "public"
17
16
 
18
- # this block will take all JS inputs, wrap them in a closure,
19
- # add some additional metadata, and concatenate them all into
20
- # application.scripts.js.
21
- match "*.js" do
22
- filter ClosureWrapper
23
- filter DataWrapper
24
- filter Rake::Pipeline::ConcatFilter, "application.scripts.js"
25
- end
17
+ input "assets" do
18
+ # this block will take all JS inputs, wrap them in a closure,
19
+ # add some additional metadata, and concatenate them all into
20
+ # application.scripts.js.
21
+ match "*.js" do
22
+ filter ClosureWrapper
23
+ filter DataWrapper
24
+ concat "application.scripts.js"
25
+ end
26
26
 
27
- # this block will take all HTML and CSS inputs, convert them
28
- # into JavaScript
29
- match "*/*.{html,css}" do
30
- filter DataWrapper
31
- filter Rake::Pipeline::ConcatFilter, "application.assets.js"
32
- end
27
+ # this block will take all HTML and CSS inputs, convert them
28
+ # into JavaScript
29
+ match "*/*.{html,css}" do
30
+ filter DataWrapper
31
+ concat "application.assets.js"
32
+ end
33
33
 
34
- match "*.js" do
35
- filter Rake::Pipeline::ConcatFilter, "application.js"
34
+ match "*.js" do
35
+ concat "application.js"
36
+ end
36
37
  end
37
38
 
38
- # copy any unprocessed files over to the output directory
39
- filter Rake::Pipeline::ConcatFilter
39
+ Each +input+ block defines a collection of files, and a pipeline
40
+ that transforms those files. Within each pipeline, you can specify
41
+ a series of filters to describe the transformations you'd like to
42
+ apply to the files.
43
+
44
+ = Upgrading from Previous Versions
45
+
46
+ The +Assetfile+ syntax has changed in version 0.6.0. In previous
47
+ versions, each +Assetfile+ defined a single pipeline, and +input+
48
+ statements would add input files to that pipeline. After version
49
+ 0.6.0, multiple pipelines can be defined in an +Assetfile+. The
50
+ +input+ method now takes a block, and this block defines a pipeline.
51
+ This means that any +match+ blocks or filters must be defined
52
+ inside an +input+ block, and no longer at the top level. For example,
53
+ this:
54
+
55
+ !!!ruby
56
+ # Prior to 0.6.0
57
+ output "public"
58
+ input "assets"
59
+
60
+ match "**/*.js" do
61
+ concat
62
+ end
40
63
 
41
- The available options are:
64
+ would now be written as:
42
65
 
43
- * {Rake::Pipeline::DSL#input input}: the directory containing your input files
44
- * {Rake::Pipeline::DSL#output output}: the directory to place your output files
45
- like to process
46
- * if you do not specify a block, the files will be
47
- copied over directly.
66
+ !!!ruby
67
+ # After 0.6.0
68
+ output "public"
69
+
70
+ input "assets" do
71
+ match "**/*.js" do
72
+ concat
73
+ end
74
+ end
48
75
 
49
76
  = Filters
50
77
 
@@ -78,7 +105,7 @@ If you had a series of input files like:
78
105
  * +app/javascripts/three.js+
79
106
 
80
107
  and you specified the +ConcatFilter+ in your
81
- +AssetFile+ like:
108
+ +Assetfile+ like:
82
109
 
83
110
  !!!ruby
84
111
  filter ConcatFilter, "application.js"
@@ -109,15 +136,17 @@ This will stop `Rake::Pipeline` from trying to interpret the
109
136
  input files as `UTF-8`, which obviously will not work on
110
137
  binary data.
111
138
 
112
- = Built-In Filters
139
+ = Filters
113
140
 
114
- At the current time, +Rake::Pipeline+ comes with a single built-in
115
- filter: {Rake::Pipeline::ConcatFilter}. Its implementation is
116
- the same as the `ConcatFilter` shown above.
141
+ +Rake::Pipeline+ comes with a built-in filter,
142
+ {Rake::Pipeline::ConcatFilter}. Its implementation is the same as the
143
+ +ConcatFilter+ above. Other filters that are useful for web development
144
+ like a +CoffeeScriptFilter+ and +SassFilter+ are available in
145
+ [rake-pipeline-web-filters](https://github.com/wycats/rake-pipeline-web-filters).
117
146
 
118
147
  = Preview Server
119
148
 
120
- To start up the preview server, run +rakep+. This will start up
149
+ To start up the preview server, run +rakep server+. This will start up
121
150
  a server that automatically recompiles files for you on the fly
122
151
  and serves up the files you need.
123
152
 
data/Rakefile CHANGED
@@ -3,10 +3,19 @@ require "bundler/gem_tasks"
3
3
 
4
4
  directory "doc"
5
5
 
6
+ desc "generate documentation"
6
7
  task :docs => Dir["lib/**"] do
7
8
  sh "devbin/yard doc --readme README.yard --hide-void-return"
8
9
  end
9
10
 
11
+ desc "generate a dependency graph from the documentation"
10
12
  task :graph => ["doc", :docs] do
11
13
  sh "devbin/yard graph --dependencies | dot -Tpng -o doc/arch.png"
12
14
  end
15
+
16
+ desc "run the specs"
17
+ task :spec do
18
+ sh "rspec -cfs spec"
19
+ end
20
+
21
+ task :default => :spec
data/bin/rakep CHANGED
@@ -1,27 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "rake-pipeline"
4
- require "rake-pipeline/middleware"
5
- require "rack/server"
6
-
7
- module Rake
8
- class Pipeline
9
- class Server < Rack::Server
10
- def app
11
- not_found = proc { [404, { "Content-Type" => "text/plain" }, ["not found"]] }
12
- config = "Assetfile"
13
-
14
- Middleware.new(not_found, config)
15
- end
16
- end
17
- end
18
- end
19
-
20
- if ARGV[0] == "build"
21
- config = "Assetfile"
22
- pipeline_source = File.read(config)
23
- pipeline = Rake::Pipeline.class_eval "build do\n#{pipeline_source}\nend", config, 1
24
- pipeline.invoke
25
- else
26
- Rake::Pipeline::Server.new.start
27
- end
4
+ Rake::Pipeline::CLI.start
@@ -0,0 +1,70 @@
1
+ module Rake
2
+ class Pipeline
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ desc "Install Rake::Pipeline in this Rails app"
6
+
7
+ def disable_asset_pipeline_railtie
8
+ say_status :config, "Updating configuration to remove asset pipeline"
9
+ gsub_file app, "require 'rails/all'", <<-RUBY.strip_heredoc
10
+ # Pick the frameworks you want:
11
+ require "active_record/railtie"
12
+ require "action_controller/railtie"
13
+ require "action_mailer/railtie"
14
+ require "active_resource/railtie"
15
+ require "rails/test_unit/railtie"
16
+ RUBY
17
+ end
18
+
19
+ # TODO: Support sprockets API
20
+ def disable_asset_pipeline_config
21
+ regex = /^\n?\s*#.*\n\s*(#\s*)?config\.assets.*\n/
22
+ gsub_file app, regex, ''
23
+ gsub_file Rails.root.join("config/environments/development.rb"), regex, ''
24
+ gsub_file Rails.root.join("config/environments/production.rb"), regex, ''
25
+ end
26
+
27
+ def remove_assets_group
28
+ regex = /^\n(#.*\n)+group :assets.*\n(.*\n)*?end\n/
29
+
30
+ gsub_file "Gemfile", regex, ''
31
+ end
32
+
33
+ def enable_assets_in_development
34
+ gsub_file "config/environments/development.rb", /^end/, "\n config.rake_pipeline_enabled = true\nend"
35
+ end
36
+
37
+ # TODO: Support asset-pipeline like API
38
+ def add_assetfile
39
+ create_file "Assetfile", <<-RUBY.strip_heredoc
40
+ # NOTE: The Assetfile will eventually be replaced with an asset-pipeline
41
+ # compatible API. This is mostly important so that plugins can easily
42
+ # inject into the pipeline.
43
+ #
44
+ # Depending on demand and how the API shakes out, we may retain the
45
+ # Assetfile API but pull in the information from the Rails API.
46
+
47
+ input "app/assets"
48
+ output "public"
49
+
50
+ match "*.js" do
51
+ concat "application.js"
52
+ end
53
+
54
+ match "*.css" do
55
+ concat "application.css"
56
+ end
57
+
58
+ # copy any remaining files
59
+ concat
60
+ RUBY
61
+ end
62
+
63
+ private
64
+ def app
65
+ @app ||= Rails.root.join("config/application.rb")
66
+ end
67
+ end
68
+ end
69
+ end
70
+
@@ -1,9 +1,15 @@
1
1
  require "rake-pipeline/file_wrapper"
2
2
  require "rake-pipeline/filter"
3
+ require "rake-pipeline/manifest_entry"
4
+ require "rake-pipeline/manifest"
5
+ require "rake-pipeline/dynamic_file_task"
3
6
  require "rake-pipeline/filters"
4
7
  require "rake-pipeline/dsl"
5
8
  require "rake-pipeline/matcher"
6
9
  require "rake-pipeline/error"
10
+ require "rake-pipeline/project"
11
+ require "rake-pipeline/cli"
12
+ require "rake-pipeline/graph"
7
13
 
8
14
  if defined?(Rails::Railtie)
9
15
  require "rake-pipeline/railtie"
@@ -77,7 +83,7 @@ module Rake
77
83
  # # first, wrap all JS files in a custom filter
78
84
  # filter ClosureFilter
79
85
  # # then, concatenate all JS files into a single file
80
- # filter Rake::Pipeline::ConcatFilter, "application.js"
86
+ # concat "application.js"
81
87
  # end
82
88
  #
83
89
  # # specify filters for css and scss files
@@ -89,7 +95,7 @@ module Rake
89
95
  # input.sub(/\.scss$/, 'css')
90
96
  # end
91
97
  # # then, concatenate all CSS files into a single file
92
- # filter Rake::Pipeline::ConcatFilter, "application.css"
98
+ # concat "application.css"
93
99
  # end
94
100
  #
95
101
  # # the remaining files not specified by a matcher (the
@@ -100,17 +106,15 @@ module Rake
100
106
  # # up until this point, as well as the HTML files.
101
107
  # end
102
108
  class Pipeline
103
- # @return [String] a glob representing the input files
104
- attr_accessor :input_glob
105
-
106
- # @return [String] the directory path for the input files.
107
- attr_reader :input_root
109
+ # @return [Hash[String, String]] the directory paths for the input files
110
+ # and their matching globs.
111
+ attr_accessor :inputs
108
112
 
109
113
  # @return [String] the directory path for the output files.
110
114
  attr_reader :output_root
111
115
 
112
- # @return [String] the directory path for temporary files.
113
- attr_reader :tmpdir
116
+ # @return [String] the directory path for temporary files
117
+ attr_accessor :tmpdir
114
118
 
115
119
  # @return [Array] an Array of Rake::Task objects. This
116
120
  # property is populated by the #generate_rake_tasks
@@ -126,10 +130,33 @@ module Rake
126
130
 
127
131
  attr_writer :input_files
128
132
 
129
- def initialize
130
- @filters = []
131
- @tmpdir = "tmp"
132
- @mutex = Mutex.new
133
+ # @return [Project] the Project that created this pipeline
134
+ attr_reader :project
135
+
136
+ # @param [Hash] options
137
+ # @option options [Hash] :inputs
138
+ # set the pipeline's {#inputs}.
139
+ # @option options [String] :tmpdir
140
+ # set the pipeline's {#tmpdir}.
141
+ # @option options [String] :output_root
142
+ # set the pipeline's {#output_root}.
143
+ # @option options [Rake::Application] :rake_application
144
+ # set the pipeline's {#rake_application}.
145
+ def initialize(options={})
146
+ @filters = []
147
+ @invoke_mutex = Mutex.new
148
+ @clean_mutex = Mutex.new
149
+ @inputs = options[:inputs] || {}
150
+ @tmpdir = options[:tmpdir] || "tmp"
151
+ @project = options[:project]
152
+
153
+ if options[:output_root]
154
+ self.output_root = options[:output_root]
155
+ end
156
+
157
+ if options[:rake_application]
158
+ self.rake_application = options[:rake_application]
159
+ end
133
160
  end
134
161
 
135
162
  # Build a new pipeline taking a block. The block will
@@ -142,7 +169,7 @@ module Rake
142
169
  # input "app/assets"
143
170
  # output "public"
144
171
  #
145
- # filter Rake::Pipeline::ConcatFilter, "app.js"
172
+ # concat "app.js"
146
173
  # end
147
174
  #
148
175
  # @see DSL the Rake::Pipeline::DSL documentation.
@@ -150,10 +177,21 @@ module Rake
150
177
  # the build block.
151
178
  #
152
179
  # @return [Rake::Pipeline] the newly configured pipeline
153
- def self.build(&block)
154
- pipeline = new
155
- DSL.evaluate(pipeline, &block) if block
156
- pipeline
180
+ def self.build(options={}, &block)
181
+ pipeline = new(options)
182
+ pipeline.build(options, &block)
183
+ end
184
+
185
+ # Evaluate a block using the Rake::Pipeline DSL against an
186
+ # existing pipeline.
187
+ #
188
+ # @see Rake::Pipeline.build
189
+ #
190
+ # @return [Rake::Pipeline] this pipeline with any modifications
191
+ # made by the given block.
192
+ def build(options={}, &block)
193
+ DSL::PipelineDSL.evaluate(self, options, &block) if block
194
+ self
157
195
  end
158
196
 
159
197
  @@tmp_id = 0
@@ -168,19 +206,12 @@ module Rake
168
206
  # @api private
169
207
  def copy(target_class=self.class, &block)
170
208
  pipeline = target_class.build(&block)
171
- pipeline.input_root = input_root
209
+ pipeline.inputs = inputs
172
210
  pipeline.tmpdir = tmpdir
173
211
  pipeline.rake_application = rake_application
174
212
  pipeline
175
213
  end
176
214
 
177
- # Set the input root of this pipeline and expand its path.
178
- #
179
- # @param [String] root this pipeline's input root
180
- def input_root=(root)
181
- @input_root = File.expand_path(root)
182
- end
183
-
184
215
  # Set the output root of this pipeline and expand its path.
185
216
  #
186
217
  # @param [String] root this pipeline's output root
@@ -195,7 +226,18 @@ module Rake
195
226
  @tmpdir = File.expand_path(dir)
196
227
  end
197
228
 
198
- # If you specify a glob for #input_glob, this method will
229
+ # Add an input directory, optionally filtering which files within
230
+ # the input directory are included.
231
+ #
232
+ # @param [String] root the input root directory; required
233
+ # @param [String] pattern a pattern to match within +root+;
234
+ # optional; defaults to "**/*"
235
+ def add_input(root, pattern = nil)
236
+ pattern ||= "**/*"
237
+ @inputs[root] = pattern
238
+ end
239
+
240
+ # If you specify #inputs, this method will
199
241
  # calculate the input files for the directory. If you supply
200
242
  # input_files directly, this method will simply return the
201
243
  # input_files you supplied.
@@ -207,13 +249,19 @@ module Rake
207
249
 
208
250
  assert_input_provided
209
251
 
210
- expanded_root = File.expand_path(input_root)
211
- files = Dir[File.join(expanded_root, input_glob)].select { |f| File.file?(f) }
252
+ result = []
212
253
 
213
- files.map do |file|
214
- relative_path = file.sub(%r{^#{Regexp.escape(expanded_root)}/}, '')
215
- FileWrapper.new(expanded_root, relative_path)
254
+ @inputs.each do |root, glob|
255
+ expanded_root = File.expand_path(root)
256
+ files = Dir[File.join(expanded_root, glob)].sort.select { |f| File.file?(f) }
257
+
258
+ files.each do |file|
259
+ relative_path = file.sub(%r{^#{Regexp.escape(expanded_root)}/}, '')
260
+ result << FileWrapper.new(expanded_root, relative_path)
261
+ end
216
262
  end
263
+
264
+ result.sort
217
265
  end
218
266
 
219
267
  # for Pipelines, this is every file, but it may be overridden
@@ -240,7 +288,10 @@ module Rake
240
288
  # @param [Array<Filter>] filters a list of filters
241
289
  # @return [void]
242
290
  def add_filters(*filters)
243
- filters.each { |filter| filter.rake_application = rake_application }
291
+ filters.each do |filter|
292
+ filter.rake_application = rake_application
293
+ filter.pipeline = self
294
+ end
244
295
  @filters.concat(filters)
245
296
  end
246
297
  alias add_filter add_filters
@@ -251,7 +302,7 @@ module Rake
251
302
  #
252
303
  # @return [void]
253
304
  def invoke
254
- @mutex.synchronize do
305
+ @invoke_mutex.synchronize do
255
306
  self.rake_application = Rake::Application.new unless @rake_application
256
307
 
257
308
  setup
@@ -266,8 +317,10 @@ module Rake
266
317
  #
267
318
  # @return [void]
268
319
  def invoke_clean
269
- @rake_tasks = @rake_application = nil
270
- invoke
320
+ @clean_mutex.synchronize do
321
+ @rake_tasks = @rake_application = nil
322
+ invoke
323
+ end
271
324
  end
272
325
 
273
326
  # Set up the filters and generate rake tasks. In general, this method
@@ -280,22 +333,6 @@ module Rake
280
333
  generate_rake_tasks
281
334
  end
282
335
 
283
- # A list of the output files that invoking this pipeline will
284
- # generate.
285
- #
286
- # @return [Array<FileWrapper>]
287
- def output_files
288
- @filters.last.output_files unless @filters.empty?
289
- end
290
-
291
- protected
292
- # Generate a new temporary directory name.
293
- #
294
- # @return [String] a unique temporary directory name
295
- def self.generate_tmpname
296
- "rake-pipeline-tmp-#{@@tmp_id += 1}"
297
- end
298
-
299
336
  # Set up the filters. This will loop through all of the filters for
300
337
  # the current pipeline and wire up their input_files and output_files.
301
338
  #
@@ -303,6 +340,7 @@ module Rake
303
340
  # set up as part of this process.
304
341
  #
305
342
  # @return [void]
343
+ # @api private
306
344
  def setup_filters
307
345
  last = @filters.last
308
346
 
@@ -327,6 +365,31 @@ module Rake
327
365
  end
328
366
  end
329
367
 
368
+ # A list of the output files that invoking this pipeline will
369
+ # generate.
370
+ #
371
+ # @return [Array<FileWrapper>]
372
+ def output_files
373
+ @filters.last.output_files unless @filters.empty?
374
+ end
375
+
376
+ # Add a final filter to the pipeline that will copy the
377
+ # pipeline's generated files to the output.
378
+ #
379
+ # @return [void]
380
+ # @api private
381
+ def finalize
382
+ add_filter(Rake::Pipeline::PipelineFinalizingFilter.new)
383
+ end
384
+
385
+ protected
386
+ # Generate a new temporary directory name.
387
+ #
388
+ # @return [String] a unique temporary directory name
389
+ def self.generate_tmpname
390
+ "rake-pipeline-tmp-#{@@tmp_id += 1}"
391
+ end
392
+
330
393
  # Generate a new temporary directory name under the main tmpdir.
331
394
  #
332
395
  # @return [void]
@@ -356,10 +419,11 @@ module Rake
356
419
  # @raise Rake::Pipeline::Error if input root or glob were missing.
357
420
  # @return [void]
358
421
  def assert_input_provided
359
- if !input_root || !input_glob
422
+ if inputs.empty?
360
423
  raise Rake::Pipeline::Error, "You cannot get input files without " \
361
424
  "first providing input files and an input root"
362
425
  end
363
426
  end
427
+
364
428
  end
365
429
  end