Monday, October 20, 2008

Creating a new Merb stack with Templater

In trying to remove Merb's dependency on ActiveSupport, Jonas Nicklas developed Templater. I'm going to show you briefly how to use it to create an ActiveRecord, TestUnit, and Prototype based Merb stack.

I'd like to make clear at this point that this is more for learning about Merb, generators, and to use as a reference point for creating other stacks. I prefer DataMapper, Rspec, and JQuery, and as such this example stack won't be supported officially or unofficially (unless someone else would like to...if so, let me know).

You can find the code here:

The first important piece is the Generators file:

scope 'merb-gen' do
dir = File.join(File.dirname(__FILE__), 'lib', 'generators/')
Merb.add_generators dir + 'merb_ar_stack'

This file is picked up by Templater and as such, when merb-gen is run, you'll see it listed as one of the available generators. Lets take a look at what merb_ar_stack actually is then.


module Merb
module Generators
class MerbArStackGenerator < AppGenerator
# ==== Paths

def self.source_root
File.join(super, 'application', 'merb_ar_stack')

def self.common_templates_dir
File.expand_path(File.join(File.dirname(__FILE__), 'templates', 'application', 'common'))

def destination_root
File.join(@destination_root, base_name)

def common_templates_dir

def testing_framework

def orm

# ==== Generator options

option :template_engine, :default => :erb,
:desc => 'Template engine to prefer for this application (one of: erb, haml).'

desc <<-DESC
This generates a "prepackaged" (or "opinionated") Merb application that uses ActiveRecord,
TestUnit, helpers, assets, mailer, caching, slices and merb-auth all out of the box.

first_argument :name, :required => true, :desc => "Application name"

# ==== Common directories & files

empty_directory :gems, 'gems'
file :thorfile do |file|
file.source = File.join(common_templates_dir, "merb.thor")
file.destination = "tasks/merb.thor"

template :rakefile do |template|
template.source = File.join(common_templates_dir, "Rakefile")
template.destination = "Rakefile"

file :gitignore do |file|
file.source = File.join(common_templates_dir, 'dotgitignore')
file.destination = ".gitignore"

file :htaccess do |file|
file.source = File.join(common_templates_dir, 'dothtaccess')
file.destination = 'public/.htaccess'

file :doctask do |file|
file.source = File.join(common_templates_dir, 'doc.thor')
file.destination = 'tasks/doc.thor'

file :prototype do |file|
file.source = File.join(common_templates_dir, 'prototype.js')
file.destination = 'public/javascripts/prototype.js'

directory :test_dir do |directory|
dir = testing_framework == :rspec ? "spec" : "test"

directory.source = File.join(source_root, dir)
directory.destination = dir

# ==== Layout specific things

# empty array means all files are considered to be just
# files, not templates
glob! "app"
glob! "autotest"
glob! "config"
glob! "doc", []
glob! "public"
glob! "lib"
glob! "merb"

invoke :layout do |generator|, options, 'application')

add 'ar-app', MerbArStackGenerator

Pretty straightforward, right? You'll see a couple places easily configured to be whatever you like, as well as listing of various files you want to include (like the prototype.js reference).

Beyond that, you'll see inside lib/generators/templates an application directory that holds a directory for the common files you'll use as well as a directory that actually lays out what the generated app will look like.

The final important piece is inside the Rakefile:

gems = [
["merb-core", "~> #{GEM_VERSION}"],
["merb-more", "~> #{GEM_VERSION}"],
["activerecord", "~> 2.1.0"]

This array is used later in the rake file by add_dependency. It takes care of bringing in the various gems needed for your stack. Take a look here to see what the official stack depends upon.

Thats pretty much it. There are other pieces that you'll want to configure things (like in the config/init.rb file inside your templates/application/merb_ar_stack folder for instance). I'd recommend forking this repo and tinkering with things a bit to get a feel for how it all fits together.

Things have been a bit busy recently, so I haven't had as much time for blogging as I'd like. I hope to write more in the near future when 1.0 is out and people are looked to really dig into things.