Merge lein-newnew into Leiningen proper.

This commit is contained in:
Anthony Grimes 2011-11-20 16:36:10 -06:00
parent 8488b22764
commit 3ecf047369
21 changed files with 294 additions and 85 deletions

View file

@ -9,6 +9,7 @@
:dependencies [[leiningen-core "2.0.0-SNAPSHOT"] :dependencies [[leiningen-core "2.0.0-SNAPSHOT"]
[clucy "0.2.2"] [clucy "0.2.2"]
[lancet "1.0.1"] [lancet "1.0.1"]
[robert/hooke "1.1.2"]] [robert/hooke "1.1.2"]
[stencil "0.2.0"]]
:disable-implicit-clean true :disable-implicit-clean true
:eval-in-leiningen true) :eval-in-leiningen true)

View file

@ -1,87 +1,29 @@
(ns leiningen.new (ns leiningen.new
"Create a new project skeleton." "Generate project scaffolding based on a template."
(:use [leiningen.core :only [abort]] (:import java.io.FileNotFoundException))
[leiningen.util.paths :only [ns->path]]
[clojure.java.io :only [file]]
[clojure.string :only [join]])
(:import (java.util Calendar)))
(defn format-settings [settings] ;; A leiningen.new template is actually just a function that generates files and
(letfn [(format-map [m] ;; directories. We have a bit of convention: we expect that each template is on
(map #(str " " %1 " " %2) ;; the classpath and is based in a .clj file at `leiningen/new/`. Making this
(map str (keys m)) ;; assumption, a user can simply give us the name of the template he wishes to
(map str (vals m))))] ;; use and we can `require` it without searching the classpath for it or doing
(apply str ;; other time consuming things.
(interpose "\n" ;;
(format-map settings))))) ;; 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 ^{:no-project-needed true}
new
"Generate scaffolding for a new project based on a template.
(defn write-project [project-dir project-name] If only one argument is passed, the default template is used and the
(let [default-settings {:dependencies [['org.clojure/clojure "1.3.0"]]} argument is treated as if it were the name of the project."
settings (merge-with #(if %2 %2 %1) ([project project-name] (leiningen.new/new project "default" project-name))
default-settings)] ([project template & args]
(.mkdirs (file project-dir)) (let [sym (symbol (str "leiningen.new." template))]
(spit (file project-dir "project.clj") (if (try (require sym)
(str "(defproject " project-name " \"1.0.0-SNAPSHOT\"\n" (catch FileNotFoundException _ true))
" :description \"FIXME: write description\"\n" (println "Could not find template" template "on the classpath.")
(format-settings (into (sorted-map) settings)) (apply (resolve (symbol (str sym "/" template))) args)))))
")" ))))
(defn write-implementation [project-dir project-clj project-ns]
(.mkdirs (.getParentFile (file project-dir "src" project-clj)))
(spit (file project-dir "src" project-clj)
(str "(ns " project-ns ")\n")))
(defn write-test [project-dir test-ns project-ns]
(.mkdirs (.getParentFile (file project-dir "test" (ns->path test-ns))))
(spit (file project-dir "test" (ns->path test-ns))
(str "(ns " (str test-ns)
"\n (:use [" project-ns "])"
"\n (:use [clojure.test]))\n\n"
"(deftest replace-me ;; FIXME: write\n (is false "
"\"No tests have been written.\"))\n")))
(defn- year []
(.get (Calendar/getInstance) Calendar/YEAR))
(defn write-readme [project-dir artifact-id]
(spit (file project-dir "README")
(join "\n\n" [(str "# " artifact-id)
"FIXME: write description"
"## Usage" "FIXME: write"
"## License" (str "Copyright (C) " (year) " FIXME")
(str "Distributed under the Eclipse Public"
" License, the same as Clojure.\n")])))
(def project-name-blacklist #"(?i)(?<!(clo|compo))jure")
(defn new
"Create a new project skeleton."
([project-name]
(leiningen.new/new project-name (name (symbol project-name))))
([project-name project-dir]
(when (re-find project-name-blacklist project-name)
(abort "Sorry, *jure names are no longer allowed."))
(try (read-string project-name)
(catch Exception _
(abort "Sorry, project names must be valid Clojure symbols.")))
(let [project-name (symbol project-name)
group-id (namespace project-name)
artifact-id (name project-name)
project-dir (-> (System/getProperty "leiningen.original.pwd")
(file project-dir)
(.getAbsolutePath ))]
(write-project project-dir project-name)
(let [prefix (.replace (str project-name) "/" ".")
project-ns (str prefix ".core")
test-ns (str prefix ".test.core")
project-clj (ns->path project-ns)]
(spit (file project-dir ".gitignore")
(apply str (interleave ["/pom.xml" "*jar" "/lib" "/classes"
"/native" "/.lein-failures" "/checkouts"
"/.lein-deps-sum"]
(repeat "\n"))))
(write-implementation project-dir project-clj project-ns)
(write-test project-dir test-ns project-ns)
(write-readme project-dir artifact-id)
(println "Created new project in:" project-dir)
(println "Look over project.clj and start coding in" project-clj)))))

View file

@ -0,0 +1,18 @@
(ns leiningen.new.default
"Generate a basic project."
(:use leiningen.new.templates))
(def render (renderer "default"))
(defn default
"A basic and general project layout."
[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)])))

View file

@ -0,0 +1,13 @@
# {{name}}
I'm an app. I sure don't do much.
## Usage
FIXME
## License
Copyright (C) 2011 FIXME
Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -0,0 +1,6 @@
(ns {{name}}.core)
(defn hi
"I don't do a whole lot."
[]
(println "Hello, World!"))

View file

@ -0,0 +1,5 @@
pom.xml
*jar
/lib/
/classes/
.lein-deps-sum

View file

@ -0,0 +1,3 @@
(defproject {{name}} "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:dependencies [[clojure "1.3.0"]])

View file

@ -0,0 +1,7 @@
(ns {{name}}.core-test
(:use clojure.test
{{name}}.core))
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))

View file

@ -0,0 +1,20 @@
(ns leiningen.new.plugin
(:use leiningen.new.templates))
(def render (renderer "plugin"))
(defn plugin
"A leiningen plugin project."
[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)])))

View file

@ -0,0 +1,13 @@
# {{name}}
I'm a Leiningen plugin. I sure don't do much.
## Usage
FIXME
## License
Copyright (C) 2011 FIXME
Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -0,0 +1,5 @@
pom.xml
*jar
/lib/
/classes/
.lein-deps-sum

View file

@ -0,0 +1,6 @@
(ns leiningen.{{unprefixed-name}})
(defn {{unprefixed-name}}
"I don't do a lot."
[project & args]
(println "Hi!"))

View file

@ -0,0 +1,3 @@
(defproject {{name}} "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:dependencies [[clojure "1.2.1"]])

View file

@ -0,0 +1,18 @@
(ns leiningen.new.template
(:use leiningen.new.templates))
(def render (renderer "template"))
(defn template
"A skeleton 'lein new' 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")])))

View file

@ -0,0 +1,13 @@
# {{name}}
A Leiningen template for FIXME.
## Usage
FIXME
## License
Copyright (C) 2011 FIXME
Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -0,0 +1 @@
(def {{name}} :foo)

View file

@ -0,0 +1,5 @@
pom.xml
*jar
/lib/
/classes/
.lein-deps-sum

View file

@ -0,0 +1,3 @@
(defproject {{name}} "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:dependencies [[clojure "1.2.1"]])

View file

@ -0,0 +1,12 @@
(ns leiningen.new.{{name}}
(:use leiningen.new.templates))
(def render (renderer "{{name}}"))
(defn {{name}}
"FIXME: write documentation"
[name]
(let [data {:name name
:sanitized (sanitize name)}]
(->files data
["src/{{placeholder}}/foo.clj" (render "foo.clj" data)])))

View file

@ -0,0 +1,87 @@
;; This API provides:
;; * 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]))
;; It is really easy to get resources off of the classpath in Clojure
;; these days.
(defn slurp-resource
"Reads the contents of a file on the classpath."
[resource-name]
(-> resource-name .getPath io/resource io/reader slurp))
;; This is so common that it really is necessary to provide a way to do it
;; easily.
(defn sanitize
"Replace hyphens with underscores."
[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)
;; 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)))
;; 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
"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"
[{: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!")))

View file

@ -0,0 +1,28 @@
(ns leiningen.templates
"List templates on the classpath."
(:use [leiningen.core.ns :only [namespaces-matching]]))
;; 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 ^{:no-project-needed true}
templates
"List available 'lein new' templates"
[project]
(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.")))))