engineer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +336 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/features/engine_rake_tasks.feature +55 -0
- data/features/engineer_install_generator.feature +47 -0
- data/features/host_install_generator.feature +54 -0
- data/features/support/env.rb +153 -0
- data/features/support/steps.rb +58 -0
- data/lib/engineer/tasks.rb +4 -0
- data/lib/engineer.rb +5 -0
- data/lib/generators/engineer/install/USAGE +10 -0
- data/lib/generators/engineer/install/install_generator.rb +101 -0
- data/lib/generators/engineer/install/templates/lib/%app_name%/engine.rb.tt +41 -0
- data/lib/generators/engineer/install/templates/lib/%app_name%.rb.tt +3 -0
- data/lib/generators/engineer/install/templates/lib/generators/%app_name%/install/USAGE.tt +5 -0
- data/lib/generators/engineer/install/templates/lib/generators/%app_name%/install/install_generator.rb.tt +19 -0
- data/lib/generators/engineer/install/templates/lib/generators/%app_name%/install/templates/%app_name%.rake.tt +167 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +13 -0
- metadata +123 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2010 Phil Smith
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,336 @@
|
|
1
|
+
= Engineer
|
2
|
+
|
3
|
+
It makes engines, get it?
|
4
|
+
|
5
|
+
== Explanation
|
6
|
+
|
7
|
+
Engines are rails apps which can be run from inside other rails apps.
|
8
|
+
|
9
|
+
Engineer is a small gem that lets a sufficiently normal rails application quickly become an
|
10
|
+
engine. It provides necessary bits of engine anatomy, and also gives the engine author a way to
|
11
|
+
publish changes (such as migrations) to users as the engine evolves. This has been a sticking
|
12
|
+
point in the past, and is one of the few remaining areas of engine support not yet covered by
|
13
|
+
rails itself.
|
14
|
+
|
15
|
+
Engineer targets rails 3 engines hosted inside rails 3 applications. If you are looking for rails
|
16
|
+
2.3 support, try the engines plugin (http://rails-engines.org).
|
17
|
+
|
18
|
+
== Getting Started
|
19
|
+
|
20
|
+
Let's say you have a new rails app named +my_engine+ that you wish to package as an engine. Drop
|
21
|
+
this in your +Gemfile+ and do the bundler thing:
|
22
|
+
|
23
|
+
<tt>gem "engineer"</tt>
|
24
|
+
|
25
|
+
A new generator named <tt>engineer:install</tt> will be available; run it.
|
26
|
+
$ rails g engineer:install
|
27
|
+
exist lib
|
28
|
+
create lib/my_engine/engine.rb
|
29
|
+
create lib/my_engine.rb
|
30
|
+
create lib/generators/my_engine/install/install_generator.rb
|
31
|
+
create lib/generators/my_engine/install/templates/my_engine.rake
|
32
|
+
create lib/generators/my_engine/install/USAGE
|
33
|
+
create app/controllers/my_engine
|
34
|
+
create app/controllers/my_engine/application_controller.rb
|
35
|
+
remove app/controllers/application_controller.rb
|
36
|
+
append Rakefile
|
37
|
+
gsub config/routes.rb
|
38
|
+
|
39
|
+
The two major take-aways from this are
|
40
|
+
1. <tt>application_controller.rb</tt> has moved under +my_engine+.
|
41
|
+
2. Your +Rakefile+ has grown a bit.
|
42
|
+
|
43
|
+
The Gory Details below explain more deeply, but for now let's just look at the new +Rakefile+
|
44
|
+
content:
|
45
|
+
$ cat Rakefile
|
46
|
+
# ...
|
47
|
+
|
48
|
+
Engineer::Tasks.new do |gem|
|
49
|
+
gem.name = "my_engine"
|
50
|
+
gem.summary = %Q{TODO: one-line summary of your engine}
|
51
|
+
gem.description = %Q{TODO: longer description of your engine}
|
52
|
+
gem.email = "TODO"
|
53
|
+
gem.homepage = "TODO"
|
54
|
+
gem.authors = ["TODO"]
|
55
|
+
gem.require_path = 'lib'
|
56
|
+
gem.files = FileList[
|
57
|
+
"[A-Z]*",
|
58
|
+
"{app,config,lib,public,spec,test,vendor}/**/*",
|
59
|
+
"db/**/*.rb"
|
60
|
+
]
|
61
|
+
|
62
|
+
# Include Bundler dependencies
|
63
|
+
Bundler.definition.dependencies.each do |dependency|
|
64
|
+
next if dependency.name == "engineer"
|
65
|
+
|
66
|
+
if (dependency.groups & [:default, :production, :staging]).any?
|
67
|
+
gem.add_dependency dependency.name, *dependency.requirement.as_list
|
68
|
+
else
|
69
|
+
gem.add_development_dependency dependency.name, *dependency.requirement.as_list
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
74
|
+
end
|
75
|
+
|
76
|
+
If you've used jeweler (http://github.com/technicalpickles/jeweler) before, this should look
|
77
|
+
eerily familiar. Engineer engines are shipped as gems, so engineer throws a (very light) wrapper
|
78
|
+
around jeweler for wrangling the gem-related tasks. Jeweler in turn keeps your gem's metadata in
|
79
|
+
the +Rakefile+, so here it is.
|
80
|
+
|
81
|
+
<b>Unlike jeweler, dependencies should still be declared in +Gemfile+, and not here.</b>
|
82
|
+
|
83
|
+
As you can see, your bundler dependencies will be included in the generated gemspec.
|
84
|
+
|
85
|
+
Let's make a gem:
|
86
|
+
$ rake build
|
87
|
+
(in /Users/phil/Public/code/my_engine)
|
88
|
+
Expected VERSION or VERSION.yml to exist. See version:write to create an initial one.
|
89
|
+
|
90
|
+
Whoops. Our gem needs to know what version it is, and we haven't told it. Start off at 0.0.0 by
|
91
|
+
$ rake version:write
|
92
|
+
(in /Users/phil/Public/code/my_engine)
|
93
|
+
Updated version: 0.0.0
|
94
|
+
|
95
|
+
$ rake build
|
96
|
+
(in /Users/phil/Public/code/my_engine)
|
97
|
+
Generated: my_engine.gemspec
|
98
|
+
my_engine.gemspec is valid.
|
99
|
+
rake aborted!
|
100
|
+
"FIXME" or "TODO" is not an author
|
101
|
+
|
102
|
+
(See full trace by running task with --trace)
|
103
|
+
|
104
|
+
Doh. Remember all those TODOs (summary, author, etc) in the Rakefile metadata? Go fill those out.
|
105
|
+
|
106
|
+
Once more:
|
107
|
+
$ rake build
|
108
|
+
(in /Users/phil/Public/code/my_engine)
|
109
|
+
Generated: my_engine.gemspec
|
110
|
+
my_engine.gemspec is valid.
|
111
|
+
WARNING: no rubyforge_project specified
|
112
|
+
Successfully built RubyGem
|
113
|
+
Name: my_engine
|
114
|
+
Version: 0.0.0
|
115
|
+
File: my_engine-0.0.0.gem
|
116
|
+
|
117
|
+
There we go, there's an engine gem sitting in <tt>pkg/</tt>, go nuts. See jeweler's documentation
|
118
|
+
for managing the version, pushing to gemcutter and other goodies.
|
119
|
+
|
120
|
+
== Installing Engine Gems
|
121
|
+
|
122
|
+
How about the other side of the fence? To install an engine into a host application, the host
|
123
|
+
author follows a similar workflow.
|
124
|
+
|
125
|
+
First, add a line to the +Gemfile+ and call bundler:
|
126
|
+
|
127
|
+
<tt>gem "my_engine"</tt>
|
128
|
+
|
129
|
+
A new generator will be available, named <tt>my_engine:install</tt>; run it.
|
130
|
+
$ rails g my_engine:install
|
131
|
+
exist lib/tasks
|
132
|
+
create lib/tasks/my_engine.rake
|
133
|
+
rake my_engine:assets my_engine:db:schema my_engine:db:migrate
|
134
|
+
rm -rf /.../host/public/my_engine
|
135
|
+
ln -s /.../gems/my_engine-0.0.0/public /.../host/public/my_engine
|
136
|
+
mkdir -p db/migrate
|
137
|
+
cp /tmp/20100428232715_my_engine_schema_after_create_comments.rb /.../host/db/migrate/20100428232715_my_engine_schema_after_create_comments.rb
|
138
|
+
|
139
|
+
This includes the engine's static assets in a subdirectory under the host's +public+. Voodoo in
|
140
|
+
the engine takes care of looking there for assets (see Gory Details below.) If your OS is
|
141
|
+
allergic to symlinks, the files are copied instead.
|
142
|
+
|
143
|
+
The engine's schema is also added as a new database migration without running it. The host author
|
144
|
+
is free to take a peek at it before deciding to <tt>rake db:migrate</tt> for real.
|
145
|
+
|
146
|
+
Run your pending migrations and fire up <tt>rails s</tt>, you're good to go.
|
147
|
+
|
148
|
+
== Managing Engine Gems
|
149
|
+
|
150
|
+
After installation, some new rake tasks are available:
|
151
|
+
$ rake -T my_engine
|
152
|
+
(in /Users/phil/Public/code/host)
|
153
|
+
rake my_engine:assets[copy] # Link (or copy) my_engine's static assets
|
154
|
+
rake my_engine:db:migrate # Import my_engine's new db migrations
|
155
|
+
rake my_engine:db:schema # Import my_engine's schema as a db migration
|
156
|
+
rake my_engine:db:seed # Load my_engine's seed data
|
157
|
+
rake my_engine:update # Import my_engine's assets and new db migrations
|
158
|
+
|
159
|
+
There are catch-all tasks as well:
|
160
|
+
$ rake -T engines
|
161
|
+
(in /Users/phil/Public/code/host)
|
162
|
+
rake engines:assets[copy] # Link (or copy) static assets from all engines
|
163
|
+
rake engines:db:migrate # Import new migrations from all engines
|
164
|
+
rake engines:db:seed # Load seed data from all engines
|
165
|
+
rake engines:update # Import assets and new db migrations from all engines
|
166
|
+
|
167
|
+
These let the host author manage the newly-installed (or updated!) engine. If the host
|
168
|
+
application revs the version of the engine gem, any new engine db migrations can be imported into
|
169
|
+
the host app with:
|
170
|
+
$ rake my_engine:db:migrate
|
171
|
+
(in /Users/phil/Public/code/host)
|
172
|
+
mkdir -p db/migrate
|
173
|
+
cp /tmp/20100428232715_my_engine_create_tags.rb /.../host/db/migrate/20100428232715_my_engine_create_tags.rb
|
174
|
+
cp /tmp/20100428232716_my_engine_create_taggings.rb /.../host/db/migrate/20100428232716_my_engine_create_taggings.rb
|
175
|
+
|
176
|
+
As before, this doesn't actually run any engine migrations but instead copies new ones (with mild
|
177
|
+
munging) to <tt>db/migrate</tt>.
|
178
|
+
|
179
|
+
The engine's static assets (stylesheets, images and so on) can be updated with:
|
180
|
+
$ rake my_engine:assets
|
181
|
+
(in /Users/phil/Public/code/host)
|
182
|
+
rm -rf /Users/phil/Public/code/host/public/my_engine
|
183
|
+
ln -s /.../gems/my_engine-0.0.1/public /.../host/public/my_engine
|
184
|
+
|
185
|
+
Even when using soft-links, updating the assets is important: you need the symlink pointing into
|
186
|
+
the correct gem version.
|
187
|
+
|
188
|
+
One can do both in a single shot with <tt>rake my_engine:update</tt>. Or with
|
189
|
+
<tt>rake engines:update</tt> to hit all engines at once.
|
190
|
+
|
191
|
+
== Gory Details
|
192
|
+
|
193
|
+
Rails' engine support has become robust with the release of rails 3. There are still a few pain
|
194
|
+
points which engineer tries to address. It does so by making some decisions for you, the engine
|
195
|
+
author. In the spirit of openness, the introduced voodoo is not buried inside the engineer gem
|
196
|
+
but copied into the engine app on installation. It's your gem, feel free to tailor it to your
|
197
|
+
needs.
|
198
|
+
|
199
|
+
Some intrepid adventurers will ask for an explanation of the generated engine's internals; here is
|
200
|
+
an overview.
|
201
|
+
|
202
|
+
=== Database Migrations
|
203
|
+
|
204
|
+
Engine database migrations are a tricky problem. Luckily for me, some very clever people already
|
205
|
+
hammered out a workable idea, which engineer implements.
|
206
|
+
|
207
|
+
The migrations are packaged in the gem along with the rest of the engine. When the host author
|
208
|
+
updates the gem, she runs the <tt><engine_name>:db:migrate</tt> rake task (directly, or indirectly
|
209
|
+
through <tt><engine_name>:update</tt>.) This copies the engine migrations into the host
|
210
|
+
application, changing a few things on the way.
|
211
|
+
|
212
|
+
Specifically:
|
213
|
+
1. The migration numbers are reassigned.
|
214
|
+
2. The engine name is inserted into the new migration name.
|
215
|
+
|
216
|
+
The numbers are reassigned so that the copied migrations preserve their relative order, and yet
|
217
|
+
occur after all previous migrations in the host application. This guarantees that the host
|
218
|
+
application has a linear schema history, by making that history explicit. Put another way, the
|
219
|
+
final host migrations should reflect the evolution of the _host_ schema, not the _engine_ schema
|
220
|
+
from which it was derived.
|
221
|
+
|
222
|
+
The engine name is inserted to avoid name collisions. It allows the engine to see what migrations
|
223
|
+
have already been copied into the host app; it will not attempt to copy them again. Changing the
|
224
|
+
migration name implies changing the contained migration class name as well: the rake task will
|
225
|
+
normally take care of it.
|
226
|
+
|
227
|
+
Interested readers could also check out
|
228
|
+
* The original discussion: https://rails.lighthouseapp.com/projects/8994/tickets/2058
|
229
|
+
* James Adam's related blog post: http://interblah.net/plugin-migrations
|
230
|
+
|
231
|
+
=== Schemas
|
232
|
+
|
233
|
+
It is recommended practice to avoid running a long string of migrations when setting up a new
|
234
|
+
database, since the migration process can become slow and brittle. Analogously, engines should be
|
235
|
+
able to (and can) create their schemas in the host database without running a long string of
|
236
|
+
migrations.
|
237
|
+
|
238
|
+
It is a very real use case for a host application author to add a new engine after the host schema
|
239
|
+
has been created and deployed to production. Deployment tools like cap and vlad know how to run
|
240
|
+
pending migrations at the right time, but they don't understand (out of the box) how to run an
|
241
|
+
engine-specific <tt>db:schema:load</tt>. It would be really great if they didn't have to.
|
242
|
+
|
243
|
+
Engineer engines satisfy these two requirements by importing the engine schema into the host
|
244
|
+
application as a migration. A naming convention is used to identify engine migrations that are
|
245
|
+
implied by the schema: the schema migration will be named something like
|
246
|
+
+my_engine_schema_after_create_posts+. This indicates that all engine migrations before (and
|
247
|
+
including) +create_posts+ should be considered to be already run. If there are no engine
|
248
|
+
migrations to skip (because the engine author removed them) then the name +my_engine_schema+ is
|
249
|
+
used instead.
|
250
|
+
|
251
|
+
No effort is made to make this schema migration reversible.
|
252
|
+
|
253
|
+
=== Seeding
|
254
|
+
|
255
|
+
Similar to loading a schema, seeding initial database rows is another task that should only be run
|
256
|
+
once per database.
|
257
|
+
|
258
|
+
Engineer engines provide two ways to load the engine's <tt>db/seeds.rb</tt>. The first is a rake
|
259
|
+
task, which the host-level <tt>rake db:seed</tt> depends on. This is meant for new databases
|
260
|
+
being set up after the engine's schema has been incorporated into the host's. All seed rows will
|
261
|
+
be loaded together at the usual time, just as all the tables were created together.
|
262
|
+
|
263
|
+
The other way seed data can be loaded is in a migration generated on engine installation. This is
|
264
|
+
appropriate for installing a new engine into an established application, such as in a production
|
265
|
+
deploy.
|
266
|
+
|
267
|
+
=== Static Assets and Asset Helpers
|
268
|
+
|
269
|
+
Separation of assets is another issue facing engines. While it is certainly useful to harness the
|
270
|
+
host's layouts and styling, engines will inescapably need their own stylesheets, scripts, etc.
|
271
|
+
|
272
|
+
Rails provides conventions about where those assets live and what they are named, and backs those
|
273
|
+
conventions up with helpers that "just work." The problem is (for example), the host and engine
|
274
|
+
master stylesheets can't both live at <tt>public/stylesheets/application.css</tt>. So we need two
|
275
|
+
separate places to keep assets, and a way of hiding this separation when it is convenient.
|
276
|
+
|
277
|
+
Many http servers serve static assets more efficiently than dynamic ones generated from frameworks
|
278
|
+
like rails. Any serious solution to the problem will need to respect this optimization. So,
|
279
|
+
assuming the web server wants to be dumb, the separate asset locations will be apparent in the
|
280
|
+
URIs. The browser only fetches the URIs the application gives it, so the application must know to
|
281
|
+
render engine asset URIs for engine assets, and host asset URIs otherwise. These URIs are created
|
282
|
+
by asset tag helpers such as +image_tag+ and +stylesheet_link_tag+.
|
283
|
+
|
284
|
+
An engine author could visit each tag and stick (for example) <tt>my_engine/</tt> in front of the
|
285
|
+
asset names, but that stinks. It also breaks the engine when run as a normal application.
|
286
|
+
Instead, the asset helpers called by the engine must create engine asset URIs only when the engine
|
287
|
+
is run as such.
|
288
|
+
|
289
|
+
Enter +asset_path+. This is an +action_controller+ configuration hook provided by rails to
|
290
|
+
control how asset URIs are generated. It can be set on a controller class, and it is inherited by
|
291
|
+
subclasses. By default, it provides rails' cache-busting ability (the <tt>?123line-noise456</tt>
|
292
|
+
on the end of your stylesheet URIs.) Engineer hijacks it to provide asset separation.
|
293
|
+
|
294
|
+
Recall that engineer's install generator moves <tt>application_controller.rb</tt> into an
|
295
|
+
engine-specific namespace: this is why. On startup, a <tt>MyEngine::Engine</tt> initializer sets
|
296
|
+
an +asset_path+ on <tt>MyEngine::ApplicationController</tt>. Since this controller is no longer
|
297
|
+
the global +ApplicationController+, we're ensured this +asset_path+ will affect not only all the
|
298
|
+
engine controllers, but only them. The initializers in <tt>MyEngine::Engine</tt> are only run
|
299
|
+
when the application is started as an engine. Thus when +my_engine+ is fired up as a normal
|
300
|
+
application, the custom +asset_path+ is not used.
|
301
|
+
|
302
|
+
Almost there. +asset_path+ can take as value either a string template (such as
|
303
|
+
<tt>"/my_engine%s"</tt>) or a lambda. If the engine and host authors were not interested in
|
304
|
+
sharing layouts and other views, <tt>"/my_engine%s"</tt> would be enough. There, all asset tags
|
305
|
+
rendered by any view from an engine controller will target the engine's assets. This goes bad
|
306
|
+
when an engine view wants to render with a host layout that includes a stylesheet: the stylesheet
|
307
|
+
URI would point into the engine, not the host.
|
308
|
+
|
309
|
+
A little more (and arguably too much) leg work can save us. The flaw is that we want the
|
310
|
+
customized URI not when requesting an engine _controller_, but when rendering an engine _view_.
|
311
|
+
If you have one around, open up <tt>lib/my_engine/engine.rb</tt>. There is another initializer in
|
312
|
+
there that duck punches <tt>ActionView::Template#render</tt>. When a template is rendered, its
|
313
|
+
+identifier+ is captured. For templates loaded from files, this is a file system path descending
|
314
|
+
from a <tt>config.paths.app.views</tt> of either the host or the engine. Thus we can distinguish
|
315
|
+
host templates from engine templates. Engine assets are generated for engine views and non-file
|
316
|
+
system views.
|
317
|
+
|
318
|
+
== Running the Tests
|
319
|
+
|
320
|
+
The (sparse) tests are written with cucumber (http://cukes.info) and can be run with just "rake".
|
321
|
+
You may need to install jeweler first.
|
322
|
+
|
323
|
+
== Thanks
|
324
|
+
|
325
|
+
This tool would not exist if the road had not already been well-paved by many others.
|
326
|
+
Specifically, James Adam's herculean effort in the rails 2.3 engines plugin has illuminated many
|
327
|
+
issues and solutions. Thanks also to the rails core team for incorporating many engine-centric
|
328
|
+
ideas into the framework, vastly simplifying what this tool needs to do. And also for just making
|
329
|
+
an awesome framework.
|
330
|
+
|
331
|
+
Finally, thanks to SEOmoz (http://seomoz.org) for letting me build this at my desk in the
|
332
|
+
afternoons instead of on the couch in the middle of the night ^_^.
|
333
|
+
|
334
|
+
== Copyright
|
335
|
+
|
336
|
+
Copyright (c) 2009-2010 Philip Smith. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "engineer"
|
8
|
+
gem.summary = %Q{Turn rails 3 applications into engines}
|
9
|
+
gem.description = "Turn your rails 3 app into an embeddable engine gem, with answers for db "\
|
10
|
+
"migrations, static assets and more."
|
11
|
+
gem.email = "phil.h.smith@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/phs/engineer"
|
13
|
+
gem.authors = ["Phil Smith"]
|
14
|
+
gem.files = FileList["[A-Z]*", "{features,lib,spec}/**/*"]
|
15
|
+
gem.add_dependency "jeweler", ">= 1.4.0"
|
16
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
17
|
+
gem.add_development_dependency "cucumber", ">= 0.6.4"
|
18
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
19
|
+
end
|
20
|
+
Jeweler::GemcutterTasks.new
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'spec/rake/spectask'
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
32
|
+
spec.libs << 'lib' << 'spec'
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
task :spec => :check_dependencies
|
38
|
+
|
39
|
+
task :default => [:spec, :cucumber]
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "engineer #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
50
|
+
|
51
|
+
require 'cucumber/rake/task'
|
52
|
+
Cucumber::Rake::Task.new do |t|
|
53
|
+
t.cucumber_opts = %w{--format pretty}
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "Remove build products"
|
57
|
+
task :clean do
|
58
|
+
rm_rf 'pkg'
|
59
|
+
end
|
60
|
+
|
61
|
+
# Some steps expect the gem to be built, so it can be added to rails projects created in tests.
|
62
|
+
task :cucumber => [:clean, :build]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,55 @@
|
|
1
|
+
Feature: Rake Tasks added to Engines
|
2
|
+
As an engine author
|
3
|
+
I want to pack my app as a gem the same way I do with jeweler
|
4
|
+
Since I probably already know how to use jeweler
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given I have a new rails app named my_engine, with the engineer gem
|
8
|
+
And I rails g engineer:install
|
9
|
+
|
10
|
+
Scenario: Creating initial VERSION file
|
11
|
+
When I try to rake version:bump:patch
|
12
|
+
Then I should see output:
|
13
|
+
"""
|
14
|
+
Expected VERSION or VERSION.yml to exist. See version:write to create an initial one.
|
15
|
+
"""
|
16
|
+
|
17
|
+
When I rake version:write
|
18
|
+
Then VERSION should contain:
|
19
|
+
"""
|
20
|
+
0.0.0
|
21
|
+
"""
|
22
|
+
|
23
|
+
Scenario: Bumping versions
|
24
|
+
Given I rake version:write
|
25
|
+
When I rake version:bump:patch
|
26
|
+
Then I should see output:
|
27
|
+
"""
|
28
|
+
Current version: 0.0.0
|
29
|
+
Updated version: 0.0.1
|
30
|
+
"""
|
31
|
+
|
32
|
+
When I rake version:bump:major
|
33
|
+
Then I should see output:
|
34
|
+
"""
|
35
|
+
Current version: 0.0.1
|
36
|
+
Updated version: 1.0.0
|
37
|
+
"""
|
38
|
+
|
39
|
+
Scenario: Building a gem
|
40
|
+
Given I rake version:write
|
41
|
+
And I fill out my Rakefile gemspec
|
42
|
+
When I rake build
|
43
|
+
Then I should see a my_engine.gemspec file
|
44
|
+
And I should see a pkg/my_engine-0.0.0.gem file
|
45
|
+
|
46
|
+
And my_engine.gemspec should contain:
|
47
|
+
"""
|
48
|
+
s.add_runtime_dependency(%q<rails>, ["${VERSION}"])
|
49
|
+
s.add_runtime_dependency(%q<sqlite3-ruby>, ["${VERSION}"])
|
50
|
+
"""
|
51
|
+
|
52
|
+
And my_engine.gemspec should not contain:
|
53
|
+
"""
|
54
|
+
s.add_runtime_dependency(%q<engineer>, ["${VERSION}"])
|
55
|
+
"""
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Feature: Engineer Installation
|
2
|
+
As an engine author
|
3
|
+
I want to install engineer painlessly
|
4
|
+
So that I can turn my rails app into an embeddable engine
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given I have a new rails app named my_engine, with the engineer gem
|
8
|
+
|
9
|
+
Scenario: Install engineer, see my new rake tasks
|
10
|
+
When I rails g engineer:install
|
11
|
+
Then I should see output:
|
12
|
+
"""
|
13
|
+
exist lib
|
14
|
+
create lib/my_engine/engine.rb
|
15
|
+
create lib/my_engine.rb
|
16
|
+
create lib/generators/my_engine/install/install_generator.rb
|
17
|
+
create lib/generators/my_engine/install/templates/my_engine.rake
|
18
|
+
create lib/generators/my_engine/install/USAGE
|
19
|
+
create app/controllers/my_engine
|
20
|
+
create app/controllers/my_engine/application_controller.rb
|
21
|
+
remove app/controllers/application_controller.rb
|
22
|
+
append Rakefile
|
23
|
+
gsub config/routes.rb
|
24
|
+
"""
|
25
|
+
|
26
|
+
And config/routes.rb should contain:
|
27
|
+
"""
|
28
|
+
Rails.application.routes.draw
|
29
|
+
"""
|
30
|
+
|
31
|
+
And app/controllers/my_engine/application_controller.rb should contain:
|
32
|
+
"""
|
33
|
+
class MyEngine::ApplicationController
|
34
|
+
"""
|
35
|
+
|
36
|
+
When I rake -T
|
37
|
+
Then I should see output:
|
38
|
+
"""
|
39
|
+
rake build # Build gem
|
40
|
+
"""
|
41
|
+
And I should see output:
|
42
|
+
"""
|
43
|
+
rake version:bump:major # Bump the gemspec by a major version.
|
44
|
+
rake version:bump:minor # Bump the gemspec by a minor version.
|
45
|
+
rake version:bump:patch # Bump the gemspec by a patch version.
|
46
|
+
rake version:write # Writes out an explicit version.
|
47
|
+
"""
|
@@ -0,0 +1,54 @@
|
|
1
|
+
Feature: Engine Installation into a Host App
|
2
|
+
As an host application author
|
3
|
+
I want to install an engine painlessly
|
4
|
+
So that I can use it in my app
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given I have a finished engine application named my_engine
|
8
|
+
And I have a new rails app named host, with the my_engine gem
|
9
|
+
|
10
|
+
Scenario: Install my_engine
|
11
|
+
When I rails g --help
|
12
|
+
Then I should see output:
|
13
|
+
"""
|
14
|
+
my_engine:install
|
15
|
+
"""
|
16
|
+
|
17
|
+
When I rails g my_engine:install
|
18
|
+
Then I should see output:
|
19
|
+
"""
|
20
|
+
exist lib/tasks
|
21
|
+
create lib/tasks/my_engine.rake
|
22
|
+
rake my_engine:install
|
23
|
+
"""
|
24
|
+
|
25
|
+
And I should see output:
|
26
|
+
"""
|
27
|
+
rm -rf /${HOST_PATH}/host/public/my_engine
|
28
|
+
ln -s /${GEM_REPO}/gems/my_engine-0.0.0/public /${HOST_PATH}/host/public/my_engine
|
29
|
+
"""
|
30
|
+
|
31
|
+
When I rake -T
|
32
|
+
Then I should see output:
|
33
|
+
"""
|
34
|
+
rake my_engine:assets[copy] # Link (or copy) my_engine's static assets
|
35
|
+
rake my_engine:db:migrate # Import my_engine's new db migrations
|
36
|
+
rake my_engine:db:schema # Import my_engine's schema as a db migration
|
37
|
+
rake my_engine:db:seed # Load my_engine's seed data
|
38
|
+
rake my_engine:update # Import my_engine's assets and new db migrations
|
39
|
+
"""
|
40
|
+
|
41
|
+
And I should see output:
|
42
|
+
"""
|
43
|
+
rake engines:assets[copy] # Link (or copy) static assets from all engines
|
44
|
+
rake engines:db:migrate # Import new migrations from all engines
|
45
|
+
rake engines:db:seed # Load seed data from all engines
|
46
|
+
rake engines:update # Import assets and new db migrations from all engines
|
47
|
+
"""
|
48
|
+
|
49
|
+
When I rake my_engine:assets[true]
|
50
|
+
Then I should see output:
|
51
|
+
"""
|
52
|
+
rm -rf /${HOST_PATH}/host/public/my_engine
|
53
|
+
cp -r /${GEM_REPO}/gems/my_engine-0.0.0/public /${HOST_PATH}/host/public/my_engine
|
54
|
+
"""
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# Inspiration shamelessly stolen from
|
2
|
+
# http://gravityblast.com/2009/08/11/testing-rails-generators-with-cucumber/
|
3
|
+
|
4
|
+
require "rubygems"
|
5
|
+
gem "railties", ">= 3.0.0.beta2"
|
6
|
+
|
7
|
+
require "fileutils"
|
8
|
+
require "tempfile"
|
9
|
+
|
10
|
+
class String
|
11
|
+
def strip_ansi
|
12
|
+
gsub /\e\[\d+m/, ''
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
17
|
+
ENGINEER_GEM_FILE = Dir["#{PROJECT_ROOT}/pkg/engineer*gem"].first
|
18
|
+
|
19
|
+
if ENV['VERBOSE'] == 'true'
|
20
|
+
include FileUtils::Verbose
|
21
|
+
else
|
22
|
+
include FileUtils
|
23
|
+
end
|
24
|
+
|
25
|
+
module Helpers
|
26
|
+
def generate_rails_app(name = 'rails_app', gem_files = [])
|
27
|
+
@current_app = name
|
28
|
+
|
29
|
+
unless File.exists?(in_workspace "apps", name)
|
30
|
+
in_workspace "apps" do
|
31
|
+
run "rails #{name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
in_workspace "apps", @current_app do
|
35
|
+
gem_files.each do |gem_file|
|
36
|
+
gem_name, version = File.basename(gem_file).match(/(.*)-(\d+\.\d+\.\d+)\.gem/)[1..2]
|
37
|
+
install_gem gem_file
|
38
|
+
File.open('Gemfile', 'a') do |gemfile|
|
39
|
+
gemfile << "gem '#{gem_name}', '#{version}'\n"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
run "#{gem_home} bundle install"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
cp_r in_workspace("apps", @current_app), in_current_app
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate(generator, options = {})
|
51
|
+
in_current_app do
|
52
|
+
run "#{gem_home} bundle exec rails g #{generator}", options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def rake(rake_task, options = {})
|
57
|
+
in_current_app do
|
58
|
+
run "#{gem_home} bundle exec rake #{rake_task}", options
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def fill_out_the_rakefile_gemspec
|
63
|
+
in_current_app do
|
64
|
+
rakefile = File.read('Rakefile')
|
65
|
+
rakefile.gsub!(
|
66
|
+
'%Q{TODO: one-line summary of your engine}', '%Q{My awesome engine}')
|
67
|
+
rakefile.gsub!(
|
68
|
+
'%Q{TODO: longer description of your engine}', '%Q{My awesome, beautiful engine}')
|
69
|
+
rakefile.gsub!('gem.email = "TODO"', 'gem.email = "me@example.com"')
|
70
|
+
rakefile.gsub!('gem.homepage = "TODO"', 'gem.homepage = "http://example.com/"')
|
71
|
+
rakefile.gsub!('gem.authors = ["TODO"]', 'gem.authors = ["Me"]')
|
72
|
+
File.open('Rakefile', 'w') { |f| f << rakefile }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def strip_wildcards(content)
|
77
|
+
Regexp.quote(content).gsub /\\\$\\\{[A-Z_]+\\\}/, '.*' # escaped ${WHAT_EVER} becomes .*
|
78
|
+
end
|
79
|
+
|
80
|
+
def latest_output
|
81
|
+
@latest_output
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def install_gem(gem_file)
|
87
|
+
@installed_gems ||= {}
|
88
|
+
@installed_gems[gem_file] ||= begin
|
89
|
+
run "#{gem_home} gem install --no-rdoc --no-ri #{gem_file}"
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def run(cmd, options = {})
|
95
|
+
log cmd
|
96
|
+
(`#{cmd} 2>&1`).tap do |output|
|
97
|
+
@latest_output = output
|
98
|
+
log output
|
99
|
+
fail unless $?.to_i == 0 or options[:may_fail]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def in_workspace(*path)
|
104
|
+
absolute_path = File.join(WORKSPACE, *path)
|
105
|
+
|
106
|
+
if block_given?
|
107
|
+
Dir.chdir(absolute_path) do
|
108
|
+
log "cd #{absolute_path}"
|
109
|
+
yield
|
110
|
+
end
|
111
|
+
else
|
112
|
+
absolute_path
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def in_current_scenario(*path, &block)
|
117
|
+
in_workspace "current_scenario", *path, &block
|
118
|
+
end
|
119
|
+
|
120
|
+
def in_current_app(*path, &block)
|
121
|
+
in_current_scenario @current_app, *path, &block
|
122
|
+
end
|
123
|
+
|
124
|
+
def gem_home
|
125
|
+
@gem_home ||= begin
|
126
|
+
repo_path = in_workspace 'gemrepo'
|
127
|
+
mkdir_p repo_path
|
128
|
+
"GEM_HOME='#{repo_path}'"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def log(message)
|
133
|
+
puts message if ENV['VERBOSE'] == 'true'
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
# TODO join from Dir::tmpdir after https://rails.lighthouseapp.com/projects/8994/tickets/4442
|
139
|
+
WORKSPACE = File.join("/tmp", "engineer-cucumber-#{$$}").tap do |tmpdir|
|
140
|
+
mkdir_p tmpdir
|
141
|
+
mkdir_p File.join(tmpdir, 'apps')
|
142
|
+
at_exit { rm_rf tmpdir }
|
143
|
+
end
|
144
|
+
|
145
|
+
Before do
|
146
|
+
mkdir_p in_current_scenario
|
147
|
+
end
|
148
|
+
|
149
|
+
After do
|
150
|
+
rm_rf in_current_scenario
|
151
|
+
end
|
152
|
+
|
153
|
+
World(Helpers)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/env')
|
2
|
+
|
3
|
+
Given %r{I have a new rails app named (.*), with the (.*) gems?} do |name, gems|
|
4
|
+
gem_files = gems.split(',').collect do |gem_name|
|
5
|
+
gem_name.strip!
|
6
|
+
if gem_name == 'engineer'
|
7
|
+
ENGINEER_GEM_FILE
|
8
|
+
else
|
9
|
+
Dir["#{in_current_scenario(gem_name)}/pkg/#{gem_name}-*.gem"].first
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
generate_rails_app name, gem_files
|
14
|
+
end
|
15
|
+
|
16
|
+
When "I rails g $generator" do |generator|
|
17
|
+
generate generator
|
18
|
+
end
|
19
|
+
|
20
|
+
When "I try to rails g $generator" do |generator|
|
21
|
+
generate generator, :may_fail => true
|
22
|
+
end
|
23
|
+
|
24
|
+
When "I rake $rake_task" do |rake_task|
|
25
|
+
rake rake_task
|
26
|
+
end
|
27
|
+
|
28
|
+
When "I try to rake $rake_task" do |rake_task|
|
29
|
+
rake rake_task, :may_fail => true
|
30
|
+
end
|
31
|
+
|
32
|
+
When "I fill out my Rakefile gemspec" do
|
33
|
+
fill_out_the_rakefile_gemspec
|
34
|
+
end
|
35
|
+
|
36
|
+
Then "I should see output:" do |command_output|
|
37
|
+
latest_output.strip_ansi.strip.should match strip_wildcards(command_output.strip)
|
38
|
+
end
|
39
|
+
|
40
|
+
Then "$file should contain:" do |file, content|
|
41
|
+
File.read(in_current_app file).should match strip_wildcards(content)
|
42
|
+
end
|
43
|
+
|
44
|
+
Then "$file should not contain:" do |file, content|
|
45
|
+
File.read(in_current_app file).should_not match strip_wildcards(content)
|
46
|
+
end
|
47
|
+
|
48
|
+
Then "I should see a $file file" do |file|
|
49
|
+
File.exists?(in_current_app file).should be_true
|
50
|
+
end
|
51
|
+
|
52
|
+
Given "I have a finished engine application named $engine" do |engine|
|
53
|
+
Given "I have a new rails app named #{engine}, with the engineer gem"
|
54
|
+
And "I rails g engineer:install"
|
55
|
+
And "I rake version:write"
|
56
|
+
And "I fill out my Rakefile gemspec"
|
57
|
+
And "I rake build"
|
58
|
+
end
|
data/lib/engineer.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
Transform the application into a gem-based engine.
|
3
|
+
|
4
|
+
The major steps are:
|
5
|
+
* creating an Engine subclass under lib/
|
6
|
+
* moving ApplicationController into a submodule
|
7
|
+
* creating an installation generator for host apps
|
8
|
+
* adding jeweler-style gem tasks to the Rakefile
|
9
|
+
|
10
|
+
Please see http://github.com/phs/engineer for details
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class Engineer
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
def self.source_root
|
6
|
+
@source_root ||= File.expand_path('../templates', __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_engine_files
|
10
|
+
directory 'lib', 'lib'
|
11
|
+
end
|
12
|
+
|
13
|
+
def namespace_application_controller
|
14
|
+
unnested_path = Rails.application.paths.app.controllers.to_a.detect do |p|
|
15
|
+
File.exists? File.join(p, 'application_controller.rb')
|
16
|
+
end
|
17
|
+
|
18
|
+
return unless unnested_path
|
19
|
+
|
20
|
+
nested_path = File.join(unnested_path, app_name)
|
21
|
+
empty_directory nested_path
|
22
|
+
|
23
|
+
create_file File.join(nested_path, 'application_controller.rb') do
|
24
|
+
content = File.read File.join(unnested_path, 'application_controller.rb')
|
25
|
+
content.gsub! "class ApplicationController", "class #{app_module}::ApplicationController"
|
26
|
+
content
|
27
|
+
end
|
28
|
+
|
29
|
+
remove_file File.join(unnested_path, 'application_controller.rb')
|
30
|
+
|
31
|
+
controller_files = Rails.application.paths.app.controllers.to_a.collect do |p|
|
32
|
+
Dir[File.join(p, "**", "*_controller.rb")]
|
33
|
+
end.flatten
|
34
|
+
|
35
|
+
controller_files.reject { |f| f =~ /application_controller\.rb$/ }.each do |file|
|
36
|
+
gsub_file file, "< ApplicationController", "< #{app_module}::ApplicationController"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def append_gemspec_to_Rakefile
|
41
|
+
in_root do
|
42
|
+
unless IO.read('Rakefile') =~ /Engineer::Tasks.new/
|
43
|
+
append_file 'Rakefile' do
|
44
|
+
<<-RAKE
|
45
|
+
|
46
|
+
Engineer::Tasks.new do |gem|
|
47
|
+
gem.name = "#{app_name}"
|
48
|
+
gem.summary = %Q{TODO: one-line summary of your engine}
|
49
|
+
gem.description = %Q{TODO: longer description of your engine}
|
50
|
+
gem.email = "TODO"
|
51
|
+
gem.homepage = "TODO"
|
52
|
+
gem.authors = ["TODO"]
|
53
|
+
gem.require_path = 'lib'
|
54
|
+
gem.files = FileList[
|
55
|
+
"[A-Z]*",
|
56
|
+
"{app,config,lib,public,spec,test,vendor}/**/*",
|
57
|
+
"db/**/*.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
# Include Bundler dependencies
|
61
|
+
Bundler.definition.dependencies.each do |dependency|
|
62
|
+
next if dependency.name == "engineer"
|
63
|
+
|
64
|
+
if (dependency.groups & [:default, :production, :staging]).any?
|
65
|
+
gem.add_dependency dependency.name, *dependency.requirement.as_list
|
66
|
+
else
|
67
|
+
gem.add_development_dependency dependency.name, *dependency.requirement.as_list
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
72
|
+
end
|
73
|
+
RAKE
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO Remove after https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4400
|
80
|
+
def tweak_route_declaration
|
81
|
+
gsub_file(File.join("config", "routes.rb"),
|
82
|
+
/#{Rails.application.class}.routes.draw/, "Rails.application.routes.draw")
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def app_name
|
88
|
+
app_module.underscore
|
89
|
+
end
|
90
|
+
|
91
|
+
def app_module
|
92
|
+
Rails.application.class.name.split('::').first
|
93
|
+
end
|
94
|
+
|
95
|
+
def engineer_version
|
96
|
+
Gem.loaded_specs["engineer"].version
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "<%= app_name %><%#%>"
|
2
|
+
require "rails"
|
3
|
+
|
4
|
+
module <%= app_module %><%#%>
|
5
|
+
class Engine < Rails::Engine
|
6
|
+
ASSET_PREFIX = "<%= app_name %><%#%>"
|
7
|
+
ENGINEER_VERSION = "<%= engineer_version %><%#%>"
|
8
|
+
|
9
|
+
initializer "<%= app_name %><%#%>.action_view.identifier_collection" do
|
10
|
+
require 'action_view/template'
|
11
|
+
class ActionView::Template
|
12
|
+
def render_with_identifier_collection(view, locals, &block)
|
13
|
+
(Thread.current[:view_identifiers] ||= []).push identifier
|
14
|
+
render_without_identifier_collection(view, locals, &block)
|
15
|
+
ensure
|
16
|
+
Thread.current[:view_identifiers].pop
|
17
|
+
end
|
18
|
+
|
19
|
+
unless instance_methods.include? "render_without_identifier_collection"
|
20
|
+
alias_method_chain :render, :identifier_collection
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def engine_view?(identifier)
|
26
|
+
@engine_views ||= Hash.new do |h, identifier|
|
27
|
+
h[identifier] = !Rails.application.paths.app.views.any? do |path|
|
28
|
+
identifier =~ /^#{Regexp.escape(path)}/
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@engine_views[identifier]
|
32
|
+
end
|
33
|
+
|
34
|
+
initializer "<%= app_name %><%#%>.asset_path" do
|
35
|
+
<%= app_module %><%#%>::ApplicationController.config.asset_path = lambda do |source|
|
36
|
+
view_identifier = (Thread.current[:view_identifiers] ||= []).last
|
37
|
+
engine_view?(view_identifier) ? "/#{ASSET_PREFIX}#{source}" : source
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module <%= app_module %><%#%>
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
def self.source_root
|
6
|
+
@source_root ||= File.expand_path('../templates', __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
def install_rake_tasks
|
10
|
+
directory '.', File.join("lib", "tasks")
|
11
|
+
end
|
12
|
+
|
13
|
+
def rake_engine_install
|
14
|
+
rake "<%= app_name %><%#%>:install"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
namespace "<%= app_name %><%#%>" do
|
2
|
+
|
3
|
+
# Migrate after schema in case there is no engine schema: at least we'll get the migrations.
|
4
|
+
task :install => %w{assets db:schema db:migrate db:migrate:seed}
|
5
|
+
|
6
|
+
desc "Import <%= app_name %><%#%>'s assets and new db migrations"
|
7
|
+
task :update => %w{assets db:migrate}
|
8
|
+
|
9
|
+
namespace :db do #<%# TODO: not everyone uses active record. %>
|
10
|
+
|
11
|
+
def host_migration(new_migration_name, &block)
|
12
|
+
require 'rails/generators/active_record'
|
13
|
+
db_migrate = File.join Rails.root, *%w{db migrate}
|
14
|
+
mkdir_p db_migrate unless File.exists? db_migrate
|
15
|
+
|
16
|
+
# TODO Kill after https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4412
|
17
|
+
sleep 1 # wait for timestamp to change.
|
18
|
+
|
19
|
+
migration_number = ActiveRecord::Generators::Base.next_migration_number(db_migrate)
|
20
|
+
new_migration = File.join db_migrate, "#{migration_number}_#{new_migration_name}.rb"
|
21
|
+
tmp_migration = File.join Dir::tmpdir, "#{migration_number}_#{new_migration_name}.rb"
|
22
|
+
|
23
|
+
File.open(tmp_migration, "w") do |f|
|
24
|
+
f << block.call
|
25
|
+
end
|
26
|
+
|
27
|
+
cp tmp_migration, new_migration
|
28
|
+
end
|
29
|
+
|
30
|
+
def import_migration(original_file, new_migration_name, &block)
|
31
|
+
host_migration new_migration_name do
|
32
|
+
block.call File.read original_file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Import <%= app_name %><%#%>'s new db migrations"
|
37
|
+
task :migrate do
|
38
|
+
require 'rails/generators/active_record'
|
39
|
+
db_migrate = File.join Rails.root, *%w{db migrate}
|
40
|
+
|
41
|
+
# Consider the set of engine migrations, in chronological order.
|
42
|
+
migrations = Dir[File.join <%= app_module %><%#%>::Engine.root, *%w{db migrate [0-9]*_*.rb}]
|
43
|
+
migrations.sort!
|
44
|
+
|
45
|
+
# See if the host already has a schema migration.
|
46
|
+
last_migration_in_schema = migrations.reverse.detect do |migration|
|
47
|
+
name = File.basename(migration).match(/\d+_(.*)\.rb/)[1]
|
48
|
+
Dir[File.join(db_migrate, "[0-9]*_<%= app_name %><%#%>_schema_after_#{name}.rb")].any?
|
49
|
+
end
|
50
|
+
|
51
|
+
# If so, do not import any migrations implied by the schema
|
52
|
+
# (recall slice! *removes* the indicated range, leaving the rest.)
|
53
|
+
migrations.slice! 0..migrations.index(last_migration_in_schema) if last_migration_in_schema
|
54
|
+
|
55
|
+
# Of the remainder
|
56
|
+
migrations.each do |old_migration|
|
57
|
+
old_name = File.basename(old_migration).match(/\d+_(.*)\.rb/)[1]
|
58
|
+
new_name = "<%= app_name %><%#%>_#{old_name}"
|
59
|
+
|
60
|
+
# Skip this single migration if we already have it
|
61
|
+
next if Dir[File.join(db_migrate, "[0-9]*_#{new_name}.rb")].any?
|
62
|
+
|
63
|
+
import_migration old_migration, new_name do |migration_source|
|
64
|
+
migration_source.gsub "class #{old_name.camelize}", "class #{new_name.camelize}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Import <%= app_name %><%#%>'s schema as a db migration"
|
70
|
+
task :schema do
|
71
|
+
schema = File.join <%= app_module %><%#%>::Engine.root, *%w{db schema.rb}
|
72
|
+
if File.exist? schema
|
73
|
+
migrations = Dir[File.join <%= app_module %><%#%>::Engine.root, *%w{db migrate [0-9]*_*.rb}]
|
74
|
+
latest_migration = migrations.sort.last
|
75
|
+
|
76
|
+
migration_name = if latest_migration
|
77
|
+
latest_migration_name = File.basename(latest_migration).match(/^\d+_(.+)\.rb$/)[1]
|
78
|
+
"<%= app_name %><%#%>_schema_after_#{latest_migration_name}"
|
79
|
+
else
|
80
|
+
"<%= app_name %><%#%>_schema"
|
81
|
+
end
|
82
|
+
|
83
|
+
import_migration schema, migration_name do |schema_source|
|
84
|
+
# Strip schema declaration
|
85
|
+
schema_source.gsub! /\A.*^ActiveRecord::Schema.define\(:version => \d+\) do/m, ''
|
86
|
+
schema_source.strip!.gsub!(/^end\Z/m, '').strip!
|
87
|
+
|
88
|
+
# Indent everything 2 and strip trailing white space
|
89
|
+
schema_source.gsub!(/^/, ' ').gsub!(/[\t ]+$/, '')
|
90
|
+
|
91
|
+
# Wrap with migration class declaration
|
92
|
+
<<-EOF
|
93
|
+
class #{migration_name.camelize} < ActiveRecord::Migration
|
94
|
+
def self.up
|
95
|
+
#{schema_source}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
EOF
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
task "migrate:seed" do
|
104
|
+
migration_name = "<%= app_name %><%#%>_seed"
|
105
|
+
if File.exist? File.join(<%= app_module %><%#%>::Engine.root, 'db', 'seeds.rb')
|
106
|
+
host_migration migration_name do
|
107
|
+
<<-EOF
|
108
|
+
class #{migration_name.camelize} < ActiveRecord::Migration
|
109
|
+
def self.up
|
110
|
+
load File.join(<%= app_module %><%#%>::Engine.root, 'db', 'seeds.rb')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
EOF
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
desc "Load <%= app_name %><%#%>'s seed data"
|
119
|
+
task :seed => :environment do
|
120
|
+
seed_file = File.join(<%= app_module %><%#%>::Engine.root, 'db', 'seeds.rb')
|
121
|
+
load(seed_file) if File.exist?(seed_file)
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
desc "Link (or copy) <%= app_name %><%#%>'s static assets"
|
127
|
+
task :assets, [:copy] => :environment do |t, args|
|
128
|
+
engine_asset_path = File.join(
|
129
|
+
Rails.application.paths.public.to_a.first,
|
130
|
+
<%= app_module %><%#%>::Engine::ASSET_PREFIX)
|
131
|
+
|
132
|
+
rm_rf engine_asset_path
|
133
|
+
host_asset_path = <%= app_module %><%#%>::Engine.paths.public.to_a.first
|
134
|
+
|
135
|
+
link = lambda do
|
136
|
+
begin
|
137
|
+
ln_s host_asset_path, engine_asset_path
|
138
|
+
true
|
139
|
+
rescue NotImplementedError
|
140
|
+
false
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
copy = lambda { cp_r host_asset_path, engine_asset_path }
|
145
|
+
|
146
|
+
not args[:copy] and link.call or copy.call
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
namespace :engines do
|
152
|
+
|
153
|
+
desc "Load seed data from all engines"
|
154
|
+
task "db:seed" => "<%= app_name %><%#%>:db:seed"
|
155
|
+
|
156
|
+
desc "Import new migrations from all engines"
|
157
|
+
task "db:migrate" => "<%= app_name %><%#%>:db:migrate"
|
158
|
+
|
159
|
+
desc "Link (or copy) static assets from all engines"
|
160
|
+
task :assets, [:copy] => "<%= app_name %><%#%>:assets"
|
161
|
+
|
162
|
+
desc "Import assets and new db migrations from all engines"
|
163
|
+
task :update => "<%= app_name %><%#%>:update"
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
task "db:seed" => "engines:db:seed"
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
gem 'jeweler', '>= 1.4.0'
|
6
|
+
|
7
|
+
require 'engineer'
|
8
|
+
require 'spec'
|
9
|
+
require 'spec/autorun'
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: engineer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Phil Smith
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-28 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: jeweler
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 4
|
30
|
+
- 0
|
31
|
+
version: 1.4.0
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 2
|
44
|
+
- 9
|
45
|
+
version: 1.2.9
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: cucumber
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
- 6
|
58
|
+
- 4
|
59
|
+
version: 0.6.4
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
description: Turn your rails 3 app into an embeddable engine gem, with answers for db migrations, static assets and more.
|
63
|
+
email: phil.h.smith@gmail.com
|
64
|
+
executables: []
|
65
|
+
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files:
|
69
|
+
- LICENSE
|
70
|
+
- README.rdoc
|
71
|
+
files:
|
72
|
+
- LICENSE
|
73
|
+
- README.rdoc
|
74
|
+
- Rakefile
|
75
|
+
- VERSION
|
76
|
+
- features/engine_rake_tasks.feature
|
77
|
+
- features/engineer_install_generator.feature
|
78
|
+
- features/host_install_generator.feature
|
79
|
+
- features/support/env.rb
|
80
|
+
- features/support/steps.rb
|
81
|
+
- lib/engineer.rb
|
82
|
+
- lib/engineer/tasks.rb
|
83
|
+
- lib/generators/engineer/install/USAGE
|
84
|
+
- lib/generators/engineer/install/install_generator.rb
|
85
|
+
- lib/generators/engineer/install/templates/lib/%app_name%.rb.tt
|
86
|
+
- lib/generators/engineer/install/templates/lib/%app_name%/engine.rb.tt
|
87
|
+
- lib/generators/engineer/install/templates/lib/generators/%app_name%/install/USAGE.tt
|
88
|
+
- lib/generators/engineer/install/templates/lib/generators/%app_name%/install/install_generator.rb.tt
|
89
|
+
- lib/generators/engineer/install/templates/lib/generators/%app_name%/install/templates/%app_name%.rake.tt
|
90
|
+
- spec/spec.opts
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
has_rdoc: true
|
93
|
+
homepage: http://github.com/phs/engineer
|
94
|
+
licenses: []
|
95
|
+
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options:
|
98
|
+
- --charset=UTF-8
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
version: "0"
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
requirements: []
|
116
|
+
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 1.3.6
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: Turn rails 3 applications into engines
|
122
|
+
test_files:
|
123
|
+
- spec/spec_helper.rb
|