Skip to content

Commit 777d41b

Browse files
anmonteirodnolen
authored andcommitted
CLJS-1960: Require CommonJS modules directly from a ClojureScript namespace
This patch addresses the first part of the solution outlined in the following design doc: https://github.com/clojure/clojurescript/wiki/Enhanced-Node.js-Modules-Support It makes possible to specify, install and require Node.js dependencies directly from ClojureScript namespaces. Future work can make it possible to support specifying these dependencies in `deps.cljs` files and handling conflict resolution between upstream foreign dependencies and foreign dependencies specified directly in the compiler options.
1 parent 1d38f73 commit 777d41b

File tree

4 files changed

+201
-70
lines changed

4 files changed

+201
-70
lines changed

src/main/cljs/cljs/module_deps.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,71 @@
11
var path = require('path');
22
var mdeps = require('module-deps');
3+
var nodeResolve = require('resolve');
4+
var browserResolve = require('browser-resolve');
35

4-
var md = mdeps({});
5-
var deps_files = [];
6+
var target = 'CLJS_TARGET';
7+
var filename = path.resolve(__dirname, 'JS_FILE');
8+
var resolver = target === 'nodejs' ? nodeResolve : browserResolve;
9+
10+
var md = mdeps({
11+
resolve: function(id, parent, cb) {
12+
// set the basedir properly so we don't try to resolve requires in the Closure
13+
// Compiler processed `node_modules` folder.
14+
parent.basedir = parent.filename === filename ? __dirname: path.dirname(parent.filename);
15+
16+
resolver(id, parent, cb);
17+
},
18+
filter: function(id) {
19+
return !nodeResolve.isCore(id);
20+
}});
21+
22+
var pkgJsons = [];
23+
var deps_files = {};
624

725
md.on('package', function (pkg) {
826
// we don't want to include the package.json for users' projects
927
if (/node_modules/.test(pkg.__dirname)) {
10-
deps_files.push({file: path.join(pkg.__dirname, 'package.json')});
28+
var pkgJson = {
29+
file: path.join(pkg.__dirname, 'package.json'),
30+
};
31+
32+
if (pkg.name != null) {
33+
pkgJson.provided = [ pkg.name ];
34+
}
35+
36+
if (pkg.main != null) {
37+
pkgJson.main = path.join(pkg.__dirname, pkg.main);
38+
}
39+
40+
pkgJsons.push(pkgJson);
1141
}
1242
});
1343

1444
md.on('file', function(file) {
15-
deps_files.push({file: file});
45+
deps_files[file] = { file: file };
1646
});
1747

1848
md.on('end', function() {
19-
process.stdout.write(JSON.stringify(deps_files));
49+
for (var i = 0; i < pkgJsons.length; i++) {
50+
var pkgJson = pkgJsons[i];
51+
52+
if (deps_files[pkgJson.main] != null && pkgJson.provided != null) {
53+
deps_files[pkgJson.main].provides = pkgJson.provided;
54+
}
55+
56+
deps_files[pkgJson.file] = { file: pkgJson.file };
57+
}
58+
59+
var values = [];
60+
for (var key in deps_files) {
61+
values.push(deps_files[key]);
62+
}
63+
64+
process.stdout.write(JSON.stringify(values));
2065
});
2166

2267
md.end({
23-
file: path.resolve(path.join(__dirname, 'JS_FILE'))
68+
file: filename,
2469
});
2570

2671
md.resume();

src/main/clojure/cljs/build/api.clj

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
[cljs.compiler :as comp]
2222
[cljs.closure :as closure]
2323
[cljs.js-deps :as js-deps])
24-
(:import [java.io
25-
File StringWriter
26-
BufferedReader
27-
Writer InputStreamReader IOException]
28-
[java.lang ProcessBuilder]))
24+
(:import [java.io File]))
2925

3026
;; =============================================================================
3127
;; Useful Utilities
@@ -219,57 +215,12 @@
219215
(binding [ana/*cljs-warning-handlers* (:warning-handlers opts ana/*cljs-warning-handlers*)]
220216
(closure/watch source opts compiler-env stop))))
221217

222-
(defn- alive? [proc]
223-
(try (.exitValue proc) false (catch IllegalThreadStateException _ true)))
224-
225-
(defn- pipe [^Process proc in ^Writer out]
226-
;; we really do want system-default encoding here
227-
(with-open [^java.io.Reader in (-> in InputStreamReader. BufferedReader.)]
228-
(loop [buf (char-array 1024)]
229-
(when (alive? proc)
230-
(try
231-
(let [len (.read in buf)]
232-
(when-not (neg? len)
233-
(.write out buf 0 len)
234-
(.flush out)))
235-
(catch IOException e
236-
(when (and (alive? proc) (not (.contains (.getMessage e) "Stream closed")))
237-
(.printStackTrace e *err*))))
238-
(recur buf)))))
239-
240218
(defn node-module-deps
241219
"EXPERIMENTAL: return the foreign libs entries as computed by running
242220
the module-deps package on the supplied JavaScript entry point. Assumes
243221
that the module-deps NPM package is either locally or globally installed."
244-
[{:keys [file]}]
245-
(let [code (string/replace
246-
(slurp (io/resource "cljs/module_deps.js"))
247-
"JS_FILE"
248-
(string/replace file
249-
(System/getProperty "user.dir") ""))
250-
proc (-> (ProcessBuilder.
251-
["node" "--eval" code])
252-
.start)
253-
is (.getInputStream proc)
254-
iw (StringWriter. (* 16 1024 1024))
255-
es (.getErrorStream proc)
256-
ew (StringWriter. (* 1024 1024))
257-
_ (do (.start
258-
(Thread.
259-
(bound-fn [] (pipe proc is iw))))
260-
(.start
261-
(Thread.
262-
(bound-fn [] (pipe proc es ew)))))
263-
err (.waitFor proc)]
264-
(if (zero? err)
265-
(into []
266-
(map (fn [{:strs [file]}] file
267-
{:file file :module-type :commonjs}))
268-
(next (json/read-str (str iw))))
269-
(do
270-
(when-not (.isAlive proc)
271-
(println (str ew)))
272-
[]))))
222+
[entry]
223+
(closure/node-module-deps entry))
273224

274225
(comment
275226
(node-module-deps
@@ -284,7 +235,7 @@
284235
the module-deps package on the supplied JavaScript entry points. Assumes
285236
that the module-deps NPM packages is either locally or globally installed."
286237
[entries]
287-
(into [] (distinct (mapcat node-module-deps entries))))
238+
(closure/node-inputs entries))
288239

289240
(comment
290241
(node-inputs

src/main/clojure/cljs/closure.clj

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
[clojure.data.json :as json]
4848
[clojure.tools.reader :as reader]
4949
[clojure.tools.reader.reader-types :as readers])
50-
(:import [java.io File BufferedInputStream StringWriter]
50+
(:import [java.lang ProcessBuilder]
51+
[java.io File BufferedInputStream BufferedReader
52+
Writer InputStreamReader IOException StringWriter]
5153
[java.net URL]
5254
[java.util.logging Level]
5355
[java.util List Random]
@@ -342,7 +344,6 @@
342344
(doseq [next (seq warnings)]
343345
(println "WARNING:" (.toString ^JSError next)))))
344346

345-
346347
;; Protocols for IJavaScript and Compilable
347348
;; ========================================
348349

@@ -353,7 +354,7 @@
353354
(-source-map [this] "Return the CLJS compiler generated JS source mapping"))
354355

355356
(extend-protocol deps/IJavaScript
356-
357+
357358
String
358359
(-foreign? [this] false)
359360
(-closure-lib? [this] false)
@@ -362,7 +363,7 @@
362363
(-provides [this] (:provides (deps/parse-js-ns (string/split-lines this))))
363364
(-requires [this] (:requires (deps/parse-js-ns (string/split-lines this))))
364365
(-source [this] this)
365-
366+
366367
clojure.lang.IPersistentMap
367368
(-foreign? [this] (:foreign this))
368369
(-closure-lib? [this] (:closure-lib this))
@@ -481,7 +482,7 @@
481482
returns a JavaScriptFile. In either case the return value satisfies
482483
IJavaScript."
483484
[^File file {:keys [output-file] :as opts}]
484-
(if output-file
485+
(if output-file
485486
(let [out-file (io/file (util/output-directory opts) output-file)]
486487
(compiled-file (comp/compile-file file out-file opts)))
487488
(let [path (.getPath ^File file)]
@@ -567,17 +568,17 @@
567568
(case (.getProtocol this)
568569
"file" (-find-sources (io/file this) opts)
569570
"jar" (find-jar-sources this opts)))
570-
571+
571572
clojure.lang.PersistentList
572573
(-compile [this opts]
573574
(compile-form-seq [this]))
574575
(-find-sources [this opts]
575576
[(ana/parse-ns [this] opts)])
576-
577+
577578
String
578579
(-compile [this opts] (-compile (io/file this) opts))
579580
(-find-sources [this opts] (-find-sources (io/file this) opts))
580-
581+
581582
clojure.lang.PersistentVector
582583
(-compile [this opts] (compile-form-seq this))
583584
(-find-sources [this opts]
@@ -1339,7 +1340,7 @@
13391340

13401341
;; optimize a ClojureScript form
13411342
(optimize {:optimizations :simple} (-compile '(def x 3) {}))
1342-
1343+
13431344
;; optimize a project
13441345
(println (->> (-compile "samples/hello/src" {})
13451346
(apply add-dependencies {})
@@ -1730,7 +1731,7 @@
17301731
(output-deps-file opts disk-sources))))
17311732

17321733
(comment
1733-
1734+
17341735
;; output unoptimized alone
17351736
(output-unoptimized {} "goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n")
17361737
;; output unoptimized with all dependencies
@@ -1916,12 +1917,19 @@
19161917
[lib])))]
19171918
(into [] (mapcat expand-lib* libs))))
19181919

1920+
(declare index-node-modules)
1921+
19191922
(defn add-implicit-options
19201923
[{:keys [optimizations output-dir]
19211924
:or {optimizations :none
19221925
output-dir "out"}
19231926
:as opts}]
1924-
(let [opts (cond-> (update opts :foreign-libs expand-libs)
1927+
(let [opts (cond-> (update opts :foreign-libs
1928+
(fn [libs]
1929+
(into []
1930+
(util/distinct-merge-by :file
1931+
(index-node-modules opts)
1932+
(expand-libs libs)))))
19251933
(:closure-defines opts)
19261934
(assoc :closure-defines
19271935
(into {}
@@ -1966,6 +1974,118 @@
19661974
(nil? (:closure-module-roots opts))
19671975
(assoc :closure-module-roots []))))
19681976

1977+
(defn- alive? [proc]
1978+
(try (.exitValue proc) false (catch IllegalThreadStateException _ true)))
1979+
1980+
(defn- pipe [^Process proc in ^Writer out]
1981+
;; we really do want system-default encoding here
1982+
(with-open [^java.io.Reader in (-> in InputStreamReader. BufferedReader.)]
1983+
(loop [buf (char-array 1024)]
1984+
(when (alive? proc)
1985+
(try
1986+
(let [len (.read in buf)]
1987+
(when-not (neg? len)
1988+
(.write out buf 0 len)
1989+
(.flush out)))
1990+
(catch IOException e
1991+
(when (and (alive? proc) (not (.contains (.getMessage e) "Stream closed")))
1992+
(.printStackTrace e *err*))))
1993+
(recur buf)))))
1994+
1995+
(defn maybe-install-node-deps!
1996+
[{:keys [npm-deps verbose] :as opts}]
1997+
(if-not (empty? npm-deps)
1998+
(do
1999+
(when (or ana/*verbose* verbose)
2000+
(util/debug-prn "Installing Node.js dependencies"))
2001+
(let [proc (-> (ProcessBuilder.
2002+
(into ["npm" "install" "module-deps"]
2003+
(map (fn [[dep version]] (str (name dep) "@" version)))
2004+
npm-deps))
2005+
.start)
2006+
is (.getInputStream proc)
2007+
iw (StringWriter. (* 16 1024 1024))
2008+
es (.getErrorStream proc)
2009+
ew (StringWriter. (* 1024 1024))
2010+
_ (do (.start
2011+
(Thread.
2012+
(bound-fn [] (pipe proc is iw))))
2013+
(.start
2014+
(Thread.
2015+
(bound-fn [] (pipe proc es ew)))))
2016+
err (.waitFor proc)]
2017+
(when (and (not (zero? err)) (not (.isAlive proc)))
2018+
(println (str ew)))
2019+
opts))
2020+
opts))
2021+
2022+
(defn node-module-deps
2023+
"EXPERIMENTAL: return the foreign libs entries as computed by running
2024+
the module-deps package on the supplied JavaScript entry point. Assumes
2025+
that the module-deps NPM package is either locally or globally installed."
2026+
([entry]
2027+
(node-module-deps entry
2028+
(when env/*compiler*
2029+
(:options @env/*compiler*))))
2030+
([{:keys [file]} {:keys [target] :as opts}]
2031+
(let [code (-> (slurp (io/resource "cljs/module_deps.js"))
2032+
(string/replace "JS_FILE" file)
2033+
(string/replace "CLJS_TARGET" (str "" (when target (name target)))))
2034+
proc (-> (ProcessBuilder.
2035+
["node" "--eval" code])
2036+
.start)
2037+
is (.getInputStream proc)
2038+
iw (StringWriter. (* 16 1024 1024))
2039+
es (.getErrorStream proc)
2040+
ew (StringWriter. (* 1024 1024))
2041+
_ (do (.start
2042+
(Thread.
2043+
(bound-fn [] (pipe proc is iw))))
2044+
(.start
2045+
(Thread.
2046+
(bound-fn [] (pipe proc es ew)))))
2047+
err (.waitFor proc)]
2048+
(if (zero? err)
2049+
(into []
2050+
(map (fn [{:strs [file provides]}] file
2051+
(merge
2052+
{:file file
2053+
:module-type :commonjs}
2054+
(when provides
2055+
{:provides provides}))))
2056+
(next (json/read-str (str iw))))
2057+
(do
2058+
(when-not (.isAlive proc)
2059+
(println (str ew)))
2060+
[])))))
2061+
2062+
(defn node-inputs
2063+
"EXPERIMENTAL: return the foreign libs entries as computed by running
2064+
the module-deps package on the supplied JavaScript entry points. Assumes
2065+
that the module-deps NPM packages is either locally or globally installed."
2066+
([entries]
2067+
(node-inputs entries
2068+
(when env/*compiler*
2069+
(:options @env/*compiler*))))
2070+
([entries opts]
2071+
(into [] (distinct (mapcat #(node-module-deps % opts) entries)))))
2072+
2073+
(defn index-node-modules
2074+
([]
2075+
(index-node-modules
2076+
(when env/*compiler*
2077+
(:options @env/*compiler*))))
2078+
([{:keys [npm-deps] :as opts}]
2079+
(let [node-modules (io/file "node_modules")]
2080+
(when (and (.exists node-modules) (.isDirectory node-modules))
2081+
(let [modules (map name (keys npm-deps))
2082+
deps-file (io/file (str (util/output-directory opts) File/separator
2083+
"cljs$node_modules.js"))]
2084+
(util/mkdirs deps-file)
2085+
(with-open [w (io/writer deps-file)]
2086+
(run! #(.write w (str "require('" % "');\n")) modules))
2087+
(node-inputs [{:file (.getAbsolutePath deps-file)}]))))))
2088+
19692089
(defn process-js-modules
19702090
"Given the current compiler options, converts JavaScript modules to Google
19712091
Closure modules and writes them to disk. Adds mapping from original module
@@ -2067,6 +2187,7 @@
20672187
(env/with-compiler-env compiler-env
20682188
(let [compiler-stats (:compiler-stats opts)
20692189
all-opts (-> opts
2190+
maybe-install-node-deps!
20702191
add-implicit-options
20712192
process-js-modules)]
20722193
(check-output-to opts)

0 commit comments

Comments
 (0)