lein-newnew

0.1.2


A Leiningen plugin for generating new projects based on templates.

dependencies

org.clojure/clojure
1.2.1
stencil
0.2.0

dev dependencies




(this space intentionally left almost blank)
 

Generate project scaffolding based on a template.

(ns leiningen.new
  (:import java.io.FileNotFoundException))

Generate scaffolding for a new project based on a template.

If only one argument is passed, the default template is used and the argument is treated as if it were the name of the project.

A lein-newnew template is actually just a function that generates files and directories. We have a bit of convention: we expect that each template is on the classpath and is based in a .clj file at leiningen/new/. Making this assumption, a user can simply give us the name of the template he wishes to use and we can require it without searching the classpath for it or doing other time consuming things.

Since our templates are just function calls just like Leiningen tasks, we can also expect that a template generation function also be named the same as the last segment of its namespace. This is what we call to generate the project. If the template's namespace is not on the classpath, we can just catch the FileNotFoundException and print a nice safe message.

(defn new
  ([project-name] (leiningen.new/new "default" project-name))
  ([template & args]
     (let [sym (symbol (str "leiningen.new." template))]
       (if (try (require sym)
                (catch FileNotFoundException _ true))
         (println "Could not find template" template "on the classpath.")
         (apply (resolve (symbol (str sym "/" template))) args)))))
 

List templates on the classpath.

(ns leiningen.templates
  (:use [leiningen.util.ns :only [namespaces-matching]]))

List available 'lein new' templates

Since we have our convention of templates always being at leiningen.new.<template>, we can easily search the classpath to find templates in the same way that Leiningen can search to find tasks. Furthermore, since our templates will always have a function named after the template that is the entry-point, we can also expect that it has the documentation for the template. We can just look up these templates on the classpath, require them, and then get the metadata off of that function to list the names and docs for all of the available templates.

(defn ^{:help-arglists '([])} templates
  []
  (println "List of 'lein new' templates on the classpath:")
  ;; There are things on the classpath at `leiningen.new` that we
  ;; don't care about here. We could use a regex here, but meh.
  (doseq [n (remove '#{leiningen.new.templates leiningen.new}
                    (namespaces-matching "leiningen.new"))]
    (require n)
    (let [n-meta (meta
                  (ns-resolve (the-ns n)
                              (symbol (last (.split (str n) "\\.")))))]
      (println (str (:name n-meta) ":")
               (or (:doc n-meta) "No documentation available.")))))
 

You can write a 'new' task yourself without any extra plugins like lein-newnew. What makes newnew so useful is the templates task for listing templates and this file. The primar problem with writing your own project scaffolding tools that are domain-specific is tht you generally have to reimplement the same things every single time. With lein-newnew, you have this little library that your templates can use. It has all the things a template is likely to need: * an easy way to generate files and namespaces * a way to render files written with a flexible template language * a way to get those files off of the classpath transparently

(ns leiningen.new.templates
  (:require [clojure.java.io :as io]
            [clojure.string :as string]
            [stencil.core :as stencil]))

Reads the contents of a file on the classpath.

It is really easy to get resources off of the classpath in Clojure these days.

(defn slurp-resource
  [resource-name]
  (-> resource-name .getPath io/resource io/reader slurp))

Replace hyphens with underscores.

This is so common that it really is necessary to provide a way to do it easily.

(defn sanitize
  [s]
  (string/replace s #"-" "_"))

It'd be silly to expect people to pull in stencil just to render a mustache string. We can just provide this function instead. In doing so, it is much more likely that a template author will have to pull in any external libraries. Though he is welcome to if he needs.

(def render-text stencil/render-string)

Create a renderer function that looks for mustache templates in the right place given the name of your template. If no data is passed, the file is simply slurped.

Templates are expected to store their mustache template files in leiningen/new/<template>/. We have our convention of where templates will be on the classpath but we still have to know what the template's name is in order to know where this directory is and thus where to look for mustache template files. Since we're likely to be rendering a number of templates, we don't want to have to pass the name of the template every single time. We've also avoided magic so far, so a dynamic var and accompanying macro to set it is not in our game plan. Instead, our function for rendering templates on the classpath will be a function returned from this higher-order function. This way, we can say the name of our template just once and our render function will always know.

(defn renderer
  "Create a renderer function that looks for mustache templates in the
   right place given the name of your template. If no data is passed, the
   file is simply slurped and the content returned unchanged."
  [name]
  (fn [template & [data]]
    (let [text (slurp-resource (io/file "leiningen" "new" name template))]
      (if data
        (render-text text data)
        text))))

Our file-generating function, ->files is very simple. We'd like to keep it that way. Sometimes you need your file paths to be templates as well. This function just renders a string that is the path to where a file is supposed to be placed by a template. It is private because you shouldn't have to call it yourself, since ->files does it for you.

(defn- template-path [name path data]
  (io/file name (render-text path data)))

Generate a file with content. path can be a java.io.File or string. It will be turned into a File regardless. Any parent directories will be created automatically. Data should include a key for :name so that the project is created in the correct directory

A template, at its core, is meant to generate files and directories that represent a project. This is our way of doing that. ->files is basically a mini-DSL for generating files. It takes your mustache template data and any number of vectors or strings. It iterates through those arguments and when it sees a vector, it treats the first element as the path to spit to and the second element as the contents to put there. If it encounters a string, it treats it as an empty directory that should be created. Any parent directories for any of our generated files and directories are created automatically. All paths are considered mustache templates and are rendered with our data. Of course, this doesn't effect paths that don't have templates in them, so it is all transparent unless you need it.

(defn ->files
  [{:keys [name] :as data} & paths]
  (if (.mkdir (io/file name))
    (doseq [path paths]
      (if (string? path)
        (.mkdirs (template-path name path data))
        (let [[path content] path
              path (template-path name path data)]
          (.mkdirs (.getParentFile path))
          (spit path content))))
    (println "Directory" name "already exists!")))
 
(ns leiningen.new.plugin
  (:use leiningen.new.templates))
(def render (renderer "plugin"))

A leiningen plugin project.

(defn plugin
  [name]
  (let [unprefixed (if (.startsWith name "lein-")
                     (subs name 5)
                     name)
        data {:name name
              :unprefixed-name unprefixed
              :sanitized (sanitize unprefixed)}]
    (println (str "Generating a skeleton Leiningen plugin called " name "."))
    (->files data
             ["project.clj" (render "project.clj" data)]
             ["README.md" (render "README.md" data)]
             [".gitignore" (render "gitignore" data)]
             ["src/leiningen/{{sanitized}}.clj" (render "name.clj" data)])))
 

Generate a basic project.

(ns leiningen.new.default
  (:use leiningen.new.templates))
(def render (renderer "default"))

A basic and general project layout.

(defn default
  [name]
  (let [data {:name name
              :sanitized (sanitize name)}]
    (println "Generating a project called" name "based on the 'default' template.")
    (->files data
             ["project.clj" (render "project.clj" data)]
             ["README.md" (render "README.md" data)]
             [".gitignore" (render "gitignore" data)]
             ["src/{{sanitized}}/core.clj" (render "core.clj" data)]
             ["test/{{sanitized}}/core_test.clj" (render "test.clj" data)])))
 
(ns leiningen.new.template
  (:use leiningen.new.templates))
(def render (renderer "template"))

A skeleton 'lein new' template.

(defn template
  [name]
  (let [data {:name name
              :sanitized (sanitize name)
              :placeholder "{{sanitized}}"}]
    (println "Generating skeleton 'lein new' template project.")
    (->files data
             ["README.md" (render "README.md" data)]
             ["project.clj" (render "project.clj" data)]
             [".gitignore" (render "gitignore" data)]
             ["src/leiningen/new/{{sanitized}}.clj" (render "temp.clj" data)]
             ["src/leiningen/new/{{sanitized}}/foo.clj" (render "foo.clj")])))