├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── src ├── lein_npm │ └── plugin.clj └── leiningen │ ├── npm.clj │ └── npm │ ├── deps.clj │ ├── node_exec.clj │ └── process.clj └── test └── leiningen └── npm_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | jdk: 3 | - openjdk6 4 | - openjdk7 5 | - oraclejdk7 6 | - oraclejdk8 7 | 8 | # http://docs.travis-ci.com/user/migrating-from-legacy/?utm_source=legacy-notice&utm_medium=banner&utm_campaign=legacy-upgrade 9 | sudo: false 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lein-npm [![Build Status](https://travis-ci.org/RyanMcG/lein-npm.svg?branch=master)](https://travis-ci.org/RyanMcG/lein-npm) 2 | 3 | Leiningen plugin for enabling Node based ClojureScript projects. 4 | 5 | ## Installation 6 | 7 | To enable lein-npm for your project, put the following in the 8 | `:plugins` vector of your `project.clj` file: 9 | 10 | ```clojure 11 | [lein-npm "0.6.2"] 12 | ``` 13 | 14 | ## Managing npm dependencies 15 | 16 | You can specify a project's npm dependencies by adding an `:npm` map to your 17 | `project.clj` with a `:dependencies` or `:devDependencies` key. These correspond 18 | to the [`"dependencies"`](https://docs.npmjs.com/files/package.json#dependencies) 19 | and [`"devDependencies"`](https://docs.npmjs.com/files/package.json#devdependencies) 20 | keys in a `package.json` file. 21 | 22 | ```clojure 23 | :npm {:dependencies [[underscore "1.4.3"] 24 | [nyancat "0.0.3"] 25 | [mongodb "1.2.7"] 26 | ;; Other types of dependencies (github, private npm, etc.) can be passed as a string 27 | ;; See npm docs though as this may change between versions. 28 | ;; https://docs.npmjs.com/files/package.json#dependencies 29 | [your-module "github-username/repo-name#commitish"]]} 30 | ``` 31 | 32 | These dependencies, and any npm dependencies of packages pulled in through the 33 | regular `:dependencies`, will be installed through npm when you run either 34 | `lein npm install` or `lein deps`. 35 | 36 | ## Transitive dependencies 37 | 38 | lein-npm looks at your project's dependencies (and their dependencies, etc) to check if there are any 39 | NPM libraries in `:dependencies` in the project.clj to install. Your testing and development 40 | libraries should go into `:devDependencies`. The only things that should go into `:dependencies` are NPM 41 | dependencies that are required for people to use your library. 42 | 43 | ## Invoking npm 44 | 45 | You can execute npm commands that require the presence of a 46 | `package.json` file using the `lein npm` command. This command creates 47 | a temporary `package.json` based on your `project.clj` before invoking 48 | the npm command you specify. The keys `name`, `description`, `version` and 49 | `dependencies` are automatically added to `package.json`. Other keys can be 50 | specified in your `project.clj` at `:package` under `:npm`.: 51 | 52 | ```clojure 53 | :npm {:package {:scripts {:test "testem"}}} 54 | ``` 55 | 56 | ```sh 57 | $ lein npm install # installs project dependencies 58 | $ lein npm ls # lists installed dependencies 59 | $ lein npm search nyancat # searches for packages containing "nyancat" 60 | ``` 61 | 62 | ## Bower dependencies 63 | 64 | [lein-bower](https://github.com/chlorinejs/lein-bower) is a related 65 | Leiningen plugin that performs the same service for 66 | [Bower](https://github.com/twitter/bower) dependencies. lein-bower 67 | itself depends on lein-npm. 68 | 69 | ## Running ClojureScript apps 70 | 71 | The plugin installs a `lein run` hook which enables you to specify a 72 | JavaScript file instead of a Clojure namespace for the `:main` key in 73 | your `project.clj`, like this: 74 | 75 | ```clojure 76 | :main "js/main.js" 77 | ``` 78 | 79 | If `:main` is a string that refers to a file that exists and ends with 80 | `.js`, it will launch this file using `npm start`, after first running 81 | `npm install` if necessary. Any command line arguments following `lein 82 | run` will be passed through to the Node process. Note that a 83 | `scripts.start` record will be automatically added to the generated 84 | `package.json` file, simply containing `node `, but 85 | you can override this using the `:package` key under `:npm` as described above. 86 | The `:main` key will still have to exist and point to a file ending in `.js`, 87 | though, or `lein run` will stay with its default behaviour. 88 | 89 | ## Changing the directory used 90 | 91 | npm does not allow you to put stuff anywhere besides `node_modules`, even 92 | if that name is [against your religion](http://web.archive.org/web/20151125135410/https://docs.npmjs.com/misc/faq#node-modules-is-the-name-of-my-deity-s-arch-rival-and-a-forbidden-word-in-my-religion-can-i-configure-npm-to-use-a-different-folder), 93 | however, you can change the root used by lein-npm to be something other than 94 | your project root like this: 95 | 96 | ```clojure 97 | :npm {:root "resources/public/js"} 98 | ``` 99 | 100 | Or you can use a keyword to look the path up in your project map: 101 | 102 | ```clojure 103 | :npm {:root :target-path} 104 | ``` 105 | 106 | ## License 107 | 108 | Copyright 2012 Bodil Stokke 109 | 110 | Licensed under the Apache License, Version 2.0 (the "License"); you 111 | may not use this file except in compliance with the License. You may 112 | obtain a copy of the License at 113 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 114 | 115 | Unless required by applicable law or agreed to in writing, software 116 | distributed under the License is distributed on an "AS IS" BASIS, 117 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 118 | implied. See the License for the specific language governing 119 | permissions and limitations under the License. 120 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lein-npm "0.7.0-rc2" 2 | :description "Manage npm dependencies for CLJS projects" 3 | :url "https://github.com/RyanMcG/lein-npm" 4 | :license {:name "Apache License, version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :dependencies [[cheshire "5.2.0"]] 7 | :profiles {:test {:dependencies [[fixturex "0.3.0"]]}} 8 | :eval-in-leiningen true) 9 | -------------------------------------------------------------------------------- /src/lein_npm/plugin.clj: -------------------------------------------------------------------------------- 1 | (ns lein-npm.plugin 2 | (:require [leiningen.npm :as npm] 3 | [leiningen.npm.node-exec :as exec])) 4 | 5 | (defn hooks [] 6 | (npm/install-hooks) 7 | (exec/install-hooks)) 8 | -------------------------------------------------------------------------------- /src/leiningen/npm.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.npm 2 | (:require [leiningen.help :as help] 3 | [leiningen.core.main :as main] 4 | [cheshire.core :as json] 5 | [clojure.java.io :as io] 6 | [clojure.set :as set] 7 | [clojure.java.shell :refer [sh]] 8 | [leiningen.npm.process :refer [exec iswin]] 9 | [leiningen.npm.deps :refer [resolve-node-deps]] 10 | [robert.hooke] 11 | [leiningen.deps])) 12 | 13 | (defn- root [project] 14 | (if-let [root (get-in project [:npm :root])] 15 | (if (keyword? root) 16 | (project root) ;; e.g. support using :target-path 17 | root) 18 | (project :root))) 19 | 20 | (defn- project-file 21 | [filename project] 22 | (io/file (root project) filename)) 23 | 24 | (def ^:const package-file-name "package.json") 25 | 26 | (defn- package-file-from-project [p] (project-file package-file-name p)) 27 | 28 | (defn- locate-npm 29 | [] 30 | (if (iswin) 31 | (sh "cmd" "/C" "for" "%i" "in" "(npm)" "do" "@echo." "%~$PATH:i") 32 | (sh "which" "npm"))) 33 | 34 | (defn environmental-consistency 35 | [project] 36 | (when 37 | (and 38 | (not (false? (get-in project [:npm :ephemeral?]))) 39 | (.exists (package-file-from-project project))) 40 | (do 41 | (println 42 | (format "Your project already has a %s file. " package-file-name) 43 | "Please remove it.") 44 | (main/abort))) 45 | (when-not (= 0 ((locate-npm) :exit)) 46 | (do 47 | (println "Unable to find npm on your path. Please install it.") 48 | (main/abort)))) 49 | 50 | (defn- invoke 51 | [project & args] 52 | (let [return-code (exec (root project) (cons "npm" args))] 53 | (when (> return-code 0) 54 | (main/exit return-code)))) 55 | 56 | (defn transform-deps 57 | [deps] 58 | (apply hash-map (flatten deps))) 59 | 60 | (defn- project->package 61 | [project] 62 | (json/generate-string 63 | (merge {:private (get-in project [:npm :private] true)} ;; by default prevent npm warnings about repository and README 64 | {:name (project :name) 65 | :description (project :description) 66 | :version (project :version) 67 | :dependencies (transform-deps (resolve-node-deps project))} 68 | (when-let [dev-deps (get-in project [:npm :devDependencies])] 69 | {:devDependencies (transform-deps dev-deps)}) 70 | (when-let [main (project :main)] 71 | {:scripts {:start (str "node " main)}}) 72 | (when-let [license (get-in project [:npm :license])] 73 | {:license license}) 74 | (when-let [repository (get-in project [:npm :repository])] 75 | {:repository repository}) 76 | (get-in project [:npm :package])) 77 | {:pretty true})) 78 | 79 | (defn- write-file 80 | [file content {:keys [ephemeral?] :or {ephemeral? true}}] 81 | (doto file 82 | (-> .getParentFile .mkdirs) 83 | (spit content)) 84 | (when ephemeral? 85 | (.deleteOnExit file))) 86 | 87 | (defmacro with-file 88 | [file content opts & forms] 89 | `(try 90 | (write-file ~file ~content ~opts) 91 | ~@forms 92 | (finally 93 | (when-not (false? (:ephemeral? ~opts)) 94 | (.delete ~file))))) 95 | 96 | (defmacro with-package-json [project & body] 97 | `(with-file (package-file-from-project ~project) 98 | (project->package ~project) 99 | (:npm ~project) 100 | ~@body)) 101 | 102 | (defn npm-debug 103 | [project] 104 | (with-package-json project 105 | (println "lein-npm generated package.json:\n") 106 | (println (slurp (package-file-from-project project))))) 107 | 108 | (def key-deprecations 109 | "Mappings from old keys to new keys in :npm." 110 | {:nodejs :package 111 | :node-dependencies :dependencies 112 | :npm-root :root}) 113 | 114 | (def deprecated-keys (set (keys key-deprecations))) 115 | 116 | (defn select-deprecated-keys 117 | "Returns a set of deprecated keys present in the given project." 118 | [project] 119 | (set/difference deprecated-keys 120 | (set/difference deprecated-keys 121 | (set (keys project))))) 122 | 123 | (defn- generate-deprecation-warning [used-key] 124 | (str used-key " is deprecated. Use " (key-deprecations used-key) 125 | " in an :npm map instead.")) 126 | 127 | (defn warn-about-deprecation [project] 128 | (if-let [used-deprecated-keys (seq (select-deprecated-keys project))] 129 | (doseq [dk used-deprecated-keys] 130 | (main/warn "WARNING:" (generate-deprecation-warning dk))))) 131 | 132 | (defn npm 133 | "Invoke the npm package manager." 134 | ([project] 135 | (environmental-consistency project) 136 | (warn-about-deprecation project) 137 | (println (help/help-for "npm")) 138 | (main/abort)) 139 | ([project & args] 140 | (environmental-consistency project) 141 | (warn-about-deprecation project) 142 | (cond 143 | (= ["pprint"] args) 144 | (npm-debug project) 145 | :else 146 | (with-package-json project 147 | (apply invoke project args))))) 148 | 149 | (defn install-deps 150 | [project] 151 | (environmental-consistency project) 152 | (warn-about-deprecation project) 153 | (with-package-json project 154 | (invoke project "install"))) 155 | 156 | ; Only run install-deps via wrap-deps once. For some reason it is being called 157 | ; multiple times with when using `lein deps` and I cannot determine why. 158 | (defonce install-locked (atom false)) 159 | 160 | (defn wrap-deps 161 | [f & args] 162 | (if @install-locked 163 | (apply f args) 164 | (do 165 | (reset! install-locked true) 166 | (let [ret (apply f args)] 167 | (install-deps (first args)) 168 | (reset! install-locked false) 169 | ret)))) 170 | 171 | (defn install-hooks [] 172 | (robert.hooke/add-hook #'leiningen.deps/deps wrap-deps)) 173 | -------------------------------------------------------------------------------- /src/leiningen/npm/deps.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.npm.deps 2 | (:require [cemerick.pomegranate.aether :as a] 3 | [clojure.java.io :as io] 4 | [leiningen.core.classpath :as cp] 5 | [leiningen.core.project :as project]) 6 | (:import [java.util.jar JarFile])) 7 | 8 | (defn- get-checkouts-project-file [root dep] 9 | (io/file root "checkouts" dep "project.clj")) 10 | 11 | ;; Based on technomany/leiningen:leiningen-core/src/leiningen/core/classpath.clj#read-dependency-project 12 | (defn- read-checkouts-project 13 | "Reads in the contents of a project.clj file for a given checkouts 14 | dependency, returning a Clojure data structure representation of the 15 | project configuration." 16 | [root dep] 17 | (let [checkouts-project-file (get-checkouts-project-file root dep)] 18 | (if (.exists checkouts-project-file) 19 | (let [checkouts-project (.getAbsolutePath checkouts-project-file)] 20 | (try (project/read checkouts-project [:default]) 21 | (catch Exception e 22 | (throw (Exception. (format "Problem loading %s" checkouts-project) e))))) 23 | (println 24 | "WARN ignoring checkouts directory" dep 25 | "as it does not contain a project.clj file.")))) 26 | 27 | (defn- scan-checkouts-projects 28 | "Searches for project.clj files under the given root path, and 29 | returns a set of project names that it finds." 30 | [root] 31 | (-> (io/file root "checkouts") 32 | (.list) 33 | (->> (keep (partial read-checkouts-project root)) 34 | (keep (comp name :name)) 35 | (set)))) 36 | 37 | (defn- find-project-form 38 | "Find the first form where the first element is the symbol defproject in the 39 | given top-level forms." 40 | [forms] 41 | (letfn [(fpf [form] 42 | (if (list? form) 43 | (if (= 'defproject (first form)) 44 | form 45 | (find-project-form form))))] 46 | (->> forms 47 | (map fpf) 48 | (keep identity) 49 | (first)))) 50 | 51 | (defn- read-project-form [input-stream] 52 | (->> (str \( (slurp input-stream) \)) 53 | (read-string) 54 | (find-project-form))) 55 | 56 | (defn- resolve-in-jar-dep 57 | "Finds dependencies in the project definition in a given jar-file using 58 | lookup-deps, if a project.clj is found in it. Nil when there are no 59 | dependencies, or when the jar's project is in the given exclusions set." 60 | [lookup-deps exclusions jar-file] 61 | (let [jar-project-entry (.getEntry jar-file "project.clj") 62 | jar-project-src (when jar-project-entry 63 | (-> jar-file 64 | (.getInputStream jar-project-entry) 65 | (read-project-form))) 66 | jar-project-map (when jar-project-src 67 | (->> jar-project-src (drop 3) (apply hash-map))) 68 | jar-project-name (when jar-project-src 69 | (name (second jar-project-src))) 70 | jar-project-deps (when jar-project-map 71 | (lookup-deps jar-project-map))] 72 | (when (not (contains? exclusions jar-project-name)) 73 | jar-project-deps))) 74 | 75 | (defn resolve-repositories [repos] 76 | (->> repos 77 | (map (fn [[name repo]] 78 | [name (leiningen.core.user/resolve-credentials repo)])) 79 | (into {}))) 80 | 81 | (defn- resolve-in-jar-deps 82 | "Finds dependencies in a project definition using lookup-deps in all the 83 | project definitions for jar dependencies of a project. Excludes any Clojure 84 | project jars that are named in a set of exclusions." 85 | [lookup-deps project exclusions] 86 | (->> (a/resolve-dependencies :coordinates (project :dependencies) 87 | :repositories (resolve-repositories (project :repositories))) 88 | (a/dependency-files) 89 | (map #(JarFile. %)) 90 | (keep (partial resolve-in-jar-dep lookup-deps exclusions)) 91 | (reduce concat))) 92 | 93 | (defn- resolve-in-checkouts-deps 94 | "Finds dependencies, using lookup-deps, in all the project.clj definitions for 95 | checkouts dependencies under a given project root." 96 | [lookup-deps root] 97 | (-> (io/file root "checkouts") 98 | (.list) 99 | (->> (keep (partial read-checkouts-project root)) 100 | (keep lookup-deps) 101 | (reduce concat)))) 102 | 103 | (defn- default-lookup-deps [project] 104 | (get-in project 105 | [:npm :dependencies] 106 | (:node-dependencies project))) 107 | 108 | (defn resolve-node-deps 109 | ([lookup-deps project] 110 | (let [deps (concat (resolve-in-jar-deps lookup-deps project (scan-checkouts-projects (:root project))) 111 | (resolve-in-checkouts-deps lookup-deps (:root project)) 112 | (lookup-deps project))] 113 | deps)) 114 | ([project] 115 | (resolve-node-deps default-lookup-deps project))) 116 | -------------------------------------------------------------------------------- /src/leiningen/npm/node_exec.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.npm.node-exec 2 | (:require [leiningen.npm :refer [install-deps npm]] 3 | [clojure.java.io :as io] 4 | [robert.hooke :as hooke] 5 | [leiningen.run])) 6 | 7 | (defn- existing-js-file-path? [path] 8 | (and (string? path) 9 | (re-matches #".*\.js$" path) 10 | (.exists (io/file path)))) 11 | 12 | (defn wrap-run 13 | [f {:keys [main] :as project} & args] 14 | (if (existing-js-file-path? main) 15 | (do 16 | (install-deps project) 17 | (apply npm project "start" args)) 18 | (apply f project args))) 19 | 20 | (defn install-hooks [] 21 | (hooke/add-hook #'leiningen.run/run wrap-run)) 22 | -------------------------------------------------------------------------------- /src/leiningen/npm/process.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.npm.process 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defn iswin 5 | [] 6 | (let [os (System/getProperty "os.name")] 7 | (-> os .toLowerCase (.contains "windows")))) 8 | 9 | (defn- process 10 | [cwd args] 11 | (let [fargs (if (iswin) (concat '("cmd" "/C") args) args) 12 | proc (ProcessBuilder. fargs)] 13 | (.directory proc (io/file cwd)) 14 | (.redirectErrorStream proc true) 15 | (.start proc))) 16 | 17 | (defn exec 18 | [cwd args] 19 | (let [proc (process cwd args)] 20 | (io/copy (.getInputStream proc) (System/out)) 21 | (.waitFor proc) 22 | (.exitValue proc))) 23 | -------------------------------------------------------------------------------- /test/leiningen/npm_test.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.npm-test 2 | (:require [leiningen.npm :refer :all] 3 | [fixturex.context :refer :all] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest-ctx test-deprecation-warnings 7 | [:project {} 8 | :wad #(with-out-str 9 | (with-redefs [leiningen.core.main/warn println] 10 | (warn-about-deprecation project)))] 11 | (letfn [(warns [msg] (is (= (wad) msg))) 12 | (deprecated [ks] (is (= (select-deprecated-keys project) ks)))] 13 | (testing "without keys" 14 | (deprecated #{}) 15 | (warns "")) 16 | (testing-ctx "with an unimportant key" [:project {:unimportant-key 1}] 17 | (deprecated #{}) 18 | (warns "")) 19 | (doseq [dk deprecated-keys] 20 | (testing-ctx (str "with deprecated key: " dk) [:project {dk :anything}] 21 | (deprecated #{dk}) 22 | (warns (str "WARNING: " dk " is deprecated. Use " (key-deprecations dk) 23 | " in an :npm map instead.\n")))))) 24 | --------------------------------------------------------------------------------