-
-
Notifications
You must be signed in to change notification settings - Fork 179
/
node.clj
268 lines (213 loc) · 8.75 KB
/
node.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
(ns shadow.build.node
(:refer-clojure :exclude [flush compile])
(:require [clojure.java.io :as io]
[clojure.string :as str]
[cljs.compiler :as comp]
[shadow.build.log :as log]
[shadow.build.api :as build-api]
[shadow.build.output :as output]
[shadow.build.closure :as closure]
[shadow.cljs.util :as util]
[shadow.build.data :as data]
[shadow.build.resource :as rc])
(:import (java.lang ProcessBuilder$Redirect)))
(defmethod log/event->str ::flush-unoptimized
[{:keys [output-file] :as ev}]
(str "Flush node script: " output-file))
(defmethod log/event->str ::flush-optimized
[{:keys [output-file] :as ev}]
(str "Flush optimized node script: " output-file))
(defn make-main-call-js [main-fn]
{:pre [(symbol? main-fn)]}
(str "\ncljs.core.apply.cljs$core$IFn$_invoke$arity$2(" (comp/munge main-fn) ", process.argv.slice(2));"))
;; set node specific build defaults applicable to all node targets
(defn set-defaults [state]
(-> state
(build-api/with-js-options
{:target :node
:use-browser-overrides false
:entry-keys ["main"]})
;; all semi-recent versions of node should be fine with es8
;; don't overwrite user choice though
(cond->
(nil? (get-in state [:shadow.build/config :compiler-options :output-feature-set]))
(assoc-in [:compiler-options :output-feature-set] :es8))))
(defn replace-goog-global [state]
(update-in state [:sources output/goog-base-id :source]
str/replace output/goog-global-snippet "goog.global = global;"))
(defn configure
[state {:keys [main output-to] :as opts}]
(let [main-ns
(namespace main)
[main-ns main-fn]
(if (nil? main-ns)
[(name main) "main"]
[main-ns (name main)])
output-to
(io/file output-to)
output-name
(.getName output-to)
main
(symbol main-ns main-fn)
node-config
(assoc opts
:main-ns (symbol main-ns)
:main-fn (symbol main-fn)
:main main
:output-to output-to)
main-call
(-> node-config :main (make-main-call-js))
module-opts
(-> opts
(select-keys [:prepend :append :prepend-js :append-js])
(update :prepend #(str "var shadow$provide = {};\n" %))
(update :prepend #(str "(function(){\n" %))
(cond->
(not (false? (:hashbang opts)))
(update :prepend #(str "#!/usr/bin/env node\n" %)))
(update :append-js str "\n" main-call)
(update :append str "\n})();\n"))]
(-> state
(assoc :node-config node-config)
(build-api/configure-modules
{:main
(assoc module-opts
:entries [(symbol main-ns)]
:depends-on #{})})
)))
(defn compile [state]
(-> state
(build-api/analyze-modules)
(build-api/compile-sources)))
(defn optimize [state]
(build-api/optimize state))
(defn closure-defines
[state]
(str "\nglobal.CLOSURE_NO_DEPS = true;\n"
"\nglobal.CLOSURE_DEFINES = " (output/closure-defines-json state) ";\n"))
(defn flush-unoptimized
[{:keys [build-modules build-sources build-options compiler-options node-config polyfill-js] :as state}]
(when (not= 1 (count build-modules))
(throw (ex-info "node builds can only have one module!" {:tag ::output :build-modules build-modules})))
(let [{:keys [cljs-runtime-path]}
build-options
{:keys [source-map]}
compiler-options
{:keys [output-to]}
node-config]
(output/flush-sources state)
(util/with-logged-time
[state {:type ::flush-unoptimized
:output-file (.getAbsolutePath output-to)}]
(let [{:keys [prepend append sources]}
(first build-modules)
output-dir-path
(-> (data/output-file state cljs-runtime-path)
(.getAbsoluteFile)
(.toPath))
output-to-path
(-> output-to
(.getAbsoluteFile)
(.getParentFile)
(.toPath))
rel-path
(-> (.relativize output-to-path output-dir-path)
(rc/normalize-name))
out
(str/join "\n"
[prepend
(str "var SHADOW_IMPORT_PATH = __dirname + '/" rel-path "';")
;; special case for node-repl which doesn't execute a file
;; but instead executes the code via stdin pipe
(str "if (__dirname == '.') { SHADOW_IMPORT_PATH = "
(-> (data/output-file state cljs-runtime-path)
(.getAbsolutePath)
(pr-str))
"; }")
"global.$CLJS = global;"
"global.shadow$provide = {};"
(when source-map
(str "try {"
"require('source-map-support').install();"
"} catch (e) {"
"console.warn('no \"source-map-support\" (run \"npm install source-map-support --save-dev\" to get it)');"
"}"))
;; this means they rely on goog.global = this AND fn.call(SHADOW_ENV, ...)
;; I eventually want to turn the "this" of shadow imports into the module
;; to match what node does.
(closure-defines state)
;; provides SHADOW_IMPORT and other things
(slurp (io/resource "shadow/cljs/node_bootstrap.txt"))
;; import all other sources
(->> sources
(map #(get-in state [:sources %]))
(map (fn [{:keys [provides output-name] :as src}]
(if (contains? provides 'goog)
(let [{:keys [js] :as out}
(data/get-output! state src)]
(str (str/replace js #"goog.global = this;" "goog.global = global;")
"\ngoog.provide = SHADOW_PROVIDE;"
"\ngoog.require = SHADOW_REQUIRE;"
(when (seq polyfill-js)
(str "\n" polyfill-js
"\nglobal.$jscomp = $jscomp;"))))
(str "SHADOW_IMPORT(" (pr-str output-name) ");"))))
(str/join "\n"))
#_(when-some [main (:main node-config)]
(let [root
(-> (str main)
(comp/munge))
root
(subs root 0 (str/index-of root "."))]
(str "var " root " = SHADOW_ENV." root ";")))
append])]
(io/make-parents output-to)
(spit output-to out))))
;; return unmodified state
state)
(defn flush-optimized
[{::closure/keys [modules] :keys [node-config] :as state}]
(let [{:keys [output-to]} node-config]
(util/with-logged-time
[state {:type ::flush-optimized
:output-file (.getAbsolutePath output-to)}]
(when (not= 1 (count modules))
(throw (ex-info "node builds can only have one module!" {:tag ::output :modules modules})))
(when-not (seq modules)
(throw (ex-info "flush before optimize?" {})))
(-> state
(assoc-in [:build-options :output-dir] (-> output-to
(.getCanonicalFile)
(.getParentFile)))
(assoc-in [::closure/modules 0 :output-name] (.getName output-to))
(output/flush-optimized))))
state)
(defmethod log/event->str ::execute!
[{:keys [args]}]
(format "Execute: %s" (pr-str args)))
(defn execute! [{:keys [node-config] :as state}]
(when (not= 1 (-> state :build-modules count))
(throw (ex-info "can only execute non modular builds" {})))
(let [{:keys [output-to]}
node-config
script-args
["node"]
pb
(doto (ProcessBuilder. script-args)
(.directory nil)
(.redirectOutput ProcessBuilder$Redirect/INHERIT)
(.redirectError ProcessBuilder$Redirect/INHERIT))]
;; not using this because we only get output once it is done
;; I prefer to see progress
;; (prn (apply shell/sh script-args))
(util/with-logged-time
[state {:type ::execute!
:args script-args}]
(let [proc
(.start pb)]
(let [out (.getOutputStream proc)]
(io/copy (io/file output-to) out)
(.close out))
;; FIXME: what if this doesn't terminate?
(let [exit-code (.waitFor proc)]
(assoc state ::exit-code exit-code))))))