engineer 0.1.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.
- 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
|