dependenciesorg.clojure/clojure |
| 1.2.1 | stencil |
| 0.2.0 |
|
(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")])))
| |