Permalink
Browse files

Merge lein-newnew into Leiningen proper.

  • Loading branch information...
1 parent 8488b22 commit 3ecf047369d73b324821e126904b8f72d0fd8e98 @Raynes committed Nov 20, 2011
View
@@ -9,6 +9,7 @@
:dependencies [[leiningen-core "2.0.0-SNAPSHOT"]
[clucy "0.2.2"]
[lancet "1.0.1"]
- [robert/hooke "1.1.2"]]
+ [robert/hooke "1.1.2"]
+ [stencil "0.2.0"]]
:disable-implicit-clean true
:eval-in-leiningen true)
View
@@ -1,87 +1,29 @@
(ns leiningen.new
- "Create a new project skeleton."
- (:use [leiningen.core :only [abort]]
- [leiningen.util.paths :only [ns->path]]
- [clojure.java.io :only [file]]
- [clojure.string :only [join]])
- (:import (java.util Calendar)))
-
-(defn format-settings [settings]
- (letfn [(format-map [m]
- (map #(str " " %1 " " %2)
- (map str (keys m))
- (map str (vals m))))]
- (apply str
- (interpose "\n"
- (format-map settings)))))
-
-(defn write-project [project-dir project-name]
- (let [default-settings {:dependencies [['org.clojure/clojure "1.3.0"]]}
- settings (merge-with #(if %2 %2 %1)
- default-settings)]
- (.mkdirs (file project-dir))
- (spit (file project-dir "project.clj")
- (str "(defproject " project-name " \"1.0.0-SNAPSHOT\"\n"
- " :description \"FIXME: write description\"\n"
- (format-settings (into (sorted-map) settings))
- ")" ))))
-
-(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)))))
+ "Generate project scaffolding based on a template."
+ (:import java.io.FileNotFoundException))
+
+;; A leiningen.new 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 ^{:no-project-needed true}
+ new
+ "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."
+ ([project project-name] (leiningen.new/new project "default" project-name))
+ ([project 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)))))
@@ -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)])))
@@ -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.
@@ -0,0 +1,6 @@
+(ns {{name}}.core)
+
+(defn hi
+ "I don't do a whole lot."
+ []
+ (println "Hello, World!"))
@@ -0,0 +1,5 @@
+pom.xml
+*jar
+/lib/
+/classes/
+.lein-deps-sum
@@ -0,0 +1,3 @@
+(defproject {{name}} "0.1.0-SNAPSHOT"
+ :description "FIXME: write description"
+ :dependencies [[clojure "1.3.0"]])
@@ -0,0 +1,7 @@
+(ns {{name}}.core-test
+ (:use clojure.test
+ {{name}}.core))
+
+(deftest a-test
+ (testing "FIXME, I fail."
+ (is (= 0 1))))
@@ -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)])))
@@ -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.
@@ -0,0 +1,5 @@
+pom.xml
+*jar
+/lib/
+/classes/
+.lein-deps-sum
@@ -0,0 +1,6 @@
+(ns leiningen.{{unprefixed-name}})
+
+(defn {{unprefixed-name}}
+ "I don't do a lot."
+ [project & args]
+ (println "Hi!"))
@@ -0,0 +1,3 @@
+(defproject {{name}} "0.1.0-SNAPSHOT"
+ :description "FIXME: write description"
+ :dependencies [[clojure "1.2.1"]])
@@ -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")])))
@@ -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.
@@ -0,0 +1 @@
+(def {{name}} :foo)
@@ -0,0 +1,5 @@
+pom.xml
+*jar
+/lib/
+/classes/
+.lein-deps-sum
@@ -0,0 +1,3 @@
+(defproject {{name}} "0.1.0-SNAPSHOT"
+ :description "FIXME: write description"
+ :dependencies [[clojure "1.2.1"]])
@@ -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)])))
@@ -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!")))
Oops, something went wrong.

0 comments on commit 3ecf047

Please sign in to comment.