Merge lein-newnew into Leiningen proper.
This commit is contained in:
parent
8488b22764
commit
3ecf047369
21 changed files with 294 additions and 85 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)))))
|
|
18
src/leiningen/new/default.clj
Normal file
18
src/leiningen/new/default.clj
Normal 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)])))
|
13
src/leiningen/new/default/README.md
Normal file
13
src/leiningen/new/default/README.md
Normal 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.
|
6
src/leiningen/new/default/core.clj
Normal file
6
src/leiningen/new/default/core.clj
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
(ns {{name}}.core)
|
||||||
|
|
||||||
|
(defn hi
|
||||||
|
"I don't do a whole lot."
|
||||||
|
[]
|
||||||
|
(println "Hello, World!"))
|
5
src/leiningen/new/default/gitignore
Normal file
5
src/leiningen/new/default/gitignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pom.xml
|
||||||
|
*jar
|
||||||
|
/lib/
|
||||||
|
/classes/
|
||||||
|
.lein-deps-sum
|
3
src/leiningen/new/default/project.clj
Normal file
3
src/leiningen/new/default/project.clj
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
(defproject {{name}} "0.1.0-SNAPSHOT"
|
||||||
|
:description "FIXME: write description"
|
||||||
|
:dependencies [[clojure "1.3.0"]])
|
7
src/leiningen/new/default/test.clj
Normal file
7
src/leiningen/new/default/test.clj
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
(ns {{name}}.core-test
|
||||||
|
(:use clojure.test
|
||||||
|
{{name}}.core))
|
||||||
|
|
||||||
|
(deftest a-test
|
||||||
|
(testing "FIXME, I fail."
|
||||||
|
(is (= 0 1))))
|
20
src/leiningen/new/plugin.clj
Normal file
20
src/leiningen/new/plugin.clj
Normal 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)])))
|
13
src/leiningen/new/plugin/README.md
Normal file
13
src/leiningen/new/plugin/README.md
Normal 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.
|
5
src/leiningen/new/plugin/gitignore
Normal file
5
src/leiningen/new/plugin/gitignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pom.xml
|
||||||
|
*jar
|
||||||
|
/lib/
|
||||||
|
/classes/
|
||||||
|
.lein-deps-sum
|
6
src/leiningen/new/plugin/name.clj
Normal file
6
src/leiningen/new/plugin/name.clj
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
(ns leiningen.{{unprefixed-name}})
|
||||||
|
|
||||||
|
(defn {{unprefixed-name}}
|
||||||
|
"I don't do a lot."
|
||||||
|
[project & args]
|
||||||
|
(println "Hi!"))
|
3
src/leiningen/new/plugin/project.clj
Normal file
3
src/leiningen/new/plugin/project.clj
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
(defproject {{name}} "0.1.0-SNAPSHOT"
|
||||||
|
:description "FIXME: write description"
|
||||||
|
:dependencies [[clojure "1.2.1"]])
|
18
src/leiningen/new/template.clj
Normal file
18
src/leiningen/new/template.clj
Normal 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")])))
|
13
src/leiningen/new/template/README.md
Normal file
13
src/leiningen/new/template/README.md
Normal 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.
|
1
src/leiningen/new/template/foo.clj
Normal file
1
src/leiningen/new/template/foo.clj
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(def {{name}} :foo)
|
5
src/leiningen/new/template/gitignore
Normal file
5
src/leiningen/new/template/gitignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pom.xml
|
||||||
|
*jar
|
||||||
|
/lib/
|
||||||
|
/classes/
|
||||||
|
.lein-deps-sum
|
3
src/leiningen/new/template/project.clj
Normal file
3
src/leiningen/new/template/project.clj
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
(defproject {{name}} "0.1.0-SNAPSHOT"
|
||||||
|
:description "FIXME: write description"
|
||||||
|
:dependencies [[clojure "1.2.1"]])
|
12
src/leiningen/new/template/temp.clj
Normal file
12
src/leiningen/new/template/temp.clj
Normal 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)])))
|
87
src/leiningen/new/templates.clj
Normal file
87
src/leiningen/new/templates.clj
Normal 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!")))
|
28
src/leiningen/templates.clj
Normal file
28
src/leiningen/templates.clj
Normal 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.")))))
|
Loading…
Reference in a new issue