Skip to content

Commit be21ba1

Browse files
authored
CLJS-3233: :refer-global + :only, :require-global (#263)
New `ns` form cases `:refer-global` and `:require-global` Global values and values in ClojureScript must always be referenced by prefixing the magic `js` namespace. None of the usual affordances for managing names are available for these cases. `:refer-global` and `:require-global` provide these missing affordances. `:refer-global` allows users to refer to JavaScript global objects. For example: (ns foo (:refer-global :only [Date])) Now `Date` can be used in the namespace w/o needing to write `js/Date`. Note that ClojureScript will track these globals values as being "tainted", i.e. externs inference will continue to work for these cases. Renames are supported: (ns foo (:refer-global :only [Date] :rename {Date MyDate})) `:require-global` allows users to treat some global JavaScript object as a library. This streamlines many cases where users may wish to avoid additional ceremony around build tooling. (ns foo (:require-global [SomeLib :as lib :refer [bar]])) All of the usul expecations for `:require` are supported, renames, sublibs etc. This feature builds upon existing `:global-exports` foreign library functionalty. Foreign lib entries can now declare `:external?` to communicate to the ClojureScript compiler that no source file exists for the entry. `:require-global` leverages this new facility. You can now simply include script tags on the page, and from ClojureScript use that library w/o any additional configuration. Both of the above cases have corresponding `ns*` macro variants permitting interactive usage at the REPL: (refer-global :only '[Date]) (require-global '[SomeLib :as lib :refer [bar]])
1 parent 00ac012 commit be21ba1

File tree

6 files changed

+228
-31
lines changed

6 files changed

+228
-31
lines changed

src/main/clojure/cljs/analyzer.cljc

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2781,6 +2781,17 @@
27812781
(if (and (.exists cljcf) (.isFile cljcf))
27822782
cljcf))))))
27832783

2784+
(defn external-dep?
2785+
"Returns true if the library is an :external? foreign dep. This means no source is provided
2786+
for the library, i.e. it will be provided by some script tag on the page, or loaded by some
2787+
other means into the JS execution environment."
2788+
#?(:cljs {:tag boolean})
2789+
[dep]
2790+
(let [js-index (:js-dependency-index @env/*compiler*)]
2791+
(if-some [[_ {:keys [foreign external?]}] (find js-index (name (-> dep lib&sublib first)))]
2792+
(and foreign external?)
2793+
false)))
2794+
27842795
(defn foreign-dep?
27852796
#?(:cljs {:tag boolean})
27862797
[dep]
@@ -2828,13 +2839,19 @@
28282839
(error env
28292840
(error-message :undeclared-ns {:ns-sym dep :js-provide (name dep)}))))))))))))
28302841

2842+
(defn global-ns? [x]
2843+
(or (= 'js x)
2844+
(= "js" (namespace x))))
2845+
28312846
(defn missing-use? [lib sym cenv]
2832-
(let [js-lib (get-in cenv [:js-dependency-index (name lib)])]
2833-
(and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found)
2834-
(not (= (get js-lib :group) :goog))
2835-
(not (get js-lib :closure-lib))
2836-
(not (node-module-dep? lib))
2837-
(not (dep-has-global-exports? lib)))))
2847+
;; ignore globals referred via :refer-global
2848+
(when-not (global-ns? lib)
2849+
(let [js-lib (get-in cenv [:js-dependency-index (name lib)])]
2850+
(and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found)
2851+
(not (= (get js-lib :group) :goog))
2852+
(not (get js-lib :closure-lib))
2853+
(not (node-module-dep? lib))
2854+
(not (dep-has-global-exports? lib))))))
28382855

28392856
(defn missing-rename? [sym cenv]
28402857
(let [lib (symbol (namespace sym))
@@ -3047,6 +3064,90 @@
30473064
ret
30483065
(recur fs ret true)))))
30493066

3067+
(defn parse-global-refer-spec
3068+
[env args]
3069+
(let [xs (filter #(-> % first (= :refer-global)) args)
3070+
cnt (count xs)]
3071+
(cond
3072+
(> cnt 1)
3073+
(throw (error env "Only one :refer-global form is allowed per namespace definition"))
3074+
3075+
(== cnt 1)
3076+
(let [[_ & {:keys [only rename] :as parsed-spec}] (first xs)
3077+
only-set (set only)
3078+
err-str "Only (:refer-global :only [names]) and optionally `:rename {from to}` specs supported.
3079+
:rename symbols must be present in :only"]
3080+
(when-not (or (empty? only)
3081+
(and (vector? only)
3082+
(every? symbol only)))
3083+
(throw (error env err-str)))
3084+
(when-not (or (empty? rename)
3085+
(and (map? rename)
3086+
(every? symbol (mapcat identity rename))
3087+
(every? only-set (keys rename))))
3088+
(throw (error env (str err-str (pr-str parsed-spec)))))
3089+
(when-not (every? #{:only :rename} (keys parsed-spec))
3090+
(throw (error env (str err-str (pr-str parsed-spec)))))
3091+
{:use (zipmap only (repeat 'js))
3092+
:rename (into {}
3093+
(map (fn [[orig new-name]]
3094+
[new-name (symbol "js" (str orig))]))
3095+
rename)}))))
3096+
3097+
(defn parse-global-require-spec
3098+
[env cenv deps aliases spec]
3099+
(if (or (symbol? spec) (string? spec))
3100+
(recur env cenv deps aliases [spec])
3101+
(do
3102+
(basic-validate-ns-spec env false spec)
3103+
(let [[lib & opts] spec
3104+
{alias :as referred :refer renamed :rename
3105+
:or {alias (if (string? lib)
3106+
(symbol (munge lib))
3107+
lib)}}
3108+
(apply hash-map opts)
3109+
referred-without-renamed (seq (remove (set (keys renamed)) referred))
3110+
[rk uk renk] [:require :use :rename]]
3111+
(when-not (or (symbol? alias) (nil? alias))
3112+
(throw
3113+
(error env
3114+
(parse-ns-error-msg spec
3115+
":as must be followed by a symbol in :require / :require-macros"))))
3116+
(when (some? alias)
3117+
(let [lib' ((:fns @aliases) alias)]
3118+
(when (and (some? lib') (not= lib lib'))
3119+
(throw (error env (parse-ns-error-msg spec ":as alias must be unique"))))
3120+
(when (= alias 'js)
3121+
(when-not (= lib (get-in @aliases [:fns 'js])) ; warn only once
3122+
(warning :js-used-as-alias env {:spec spec})))
3123+
(swap! aliases update-in [:fns] conj [alias lib])))
3124+
(when-not (or (and (sequential? referred)
3125+
(every? symbol? referred))
3126+
(nil? referred))
3127+
(throw
3128+
(error env
3129+
(parse-ns-error-msg spec
3130+
":refer must be followed by a sequence of symbols in :require / :require-macros"))))
3131+
(swap! deps conj lib)
3132+
(let [ret (merge
3133+
(when (some? alias)
3134+
{rk (merge {alias lib} {lib lib})})
3135+
(when (some? referred-without-renamed)
3136+
{uk (apply hash-map (interleave referred-without-renamed (repeat lib)))})
3137+
(when (some? renamed)
3138+
{renk (reduce (fn [m [original renamed]]
3139+
(when-not (some #{original} referred)
3140+
(throw (error env
3141+
(str "Renamed symbol " original " not referred"))))
3142+
(assoc m renamed (symbol (str lib) (str original))))
3143+
{} renamed)}))]
3144+
(swap! cenv assoc-in [:js-dependency-index (str lib)]
3145+
{:external? true
3146+
:foreign true
3147+
:provides [(str lib)]
3148+
:global-exports {lib lib}})
3149+
ret)))))
3150+
30503151
(defn parse-require-spec [env macros? deps aliases spec]
30513152
(if (or (symbol? spec) (string? spec))
30523153
(recur env macros? deps aliases [spec])
@@ -3300,6 +3401,10 @@
33003401
(select-keys new deep-merge-keys))))
33013402
new))
33023403

3404+
(def ns-spec-cases
3405+
#{:use :use-macros :require :require-macros
3406+
:import :refer-global :require-global})
3407+
33033408
(defmethod parse 'ns
33043409
[_ env [_ name & args :as form] _ opts]
33053410
(when-not *allow-ns*
@@ -3334,6 +3439,7 @@
33343439
core-renames (reduce (fn [m [original renamed]]
33353440
(assoc m renamed (symbol "cljs.core" (str original))))
33363441
{} core-renames)
3442+
{global-uses :use global-renames :rename} (parse-global-refer-spec env args)
33373443
deps (atom [])
33383444
;; as-aliases can only be used *once* because they are about the reader
33393445
aliases (atom {:fns as-aliases :macros as-aliases})
@@ -3343,17 +3449,18 @@
33433449
(partial use->require env))
33443450
:use-macros (comp (partial parse-require-spec env true deps aliases)
33453451
(partial use->require env))
3346-
:import (partial parse-import-spec env deps)}
3347-
valid-forms (atom #{:use :use-macros :require :require-macros :import})
3452+
:import (partial parse-import-spec env deps)
3453+
:require-global #(parse-global-require-spec env env/*compiler* deps aliases %)}
3454+
valid-forms (atom #{:use :use-macros :require :require-macros :require-global :import})
33483455
reload (atom {:use nil :require nil :use-macros nil :require-macros nil})
33493456
reloads (atom {})
33503457
{uses :use requires :require renames :rename
33513458
use-macros :use-macros require-macros :require-macros
33523459
rename-macros :rename-macros imports :import :as params}
33533460
(reduce
33543461
(fn [m [k & libs :as libspec]]
3355-
(when-not (#{:use :use-macros :require :require-macros :import} k)
3356-
(throw (error env (str "Only :refer-clojure, :require, :require-macros, :use, :use-macros, and :import libspecs supported. Got " libspec " instead."))))
3462+
(when-not (#{:use :use-macros :require :require-macros :require-global :import} k)
3463+
(throw (error env (str "Only :refer-clojure, :require, :require-macros, :use, :use-macros, :require-global and :import libspecs supported. Got " libspec " instead."))))
33573464
(when-not (@valid-forms k)
33583465
(throw (error env (str "Only one " k " form is allowed per namespace definition"))))
33593466
(swap! valid-forms disj k)
@@ -3370,7 +3477,7 @@
33703477
(apply merge-with merge m
33713478
(map (spec-parsers k)
33723479
(remove #{:reload :reload-all} libs))))
3373-
{} (remove (fn [[r]] (= r :refer-clojure)) args))
3480+
{} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args))
33743481
;; patch `require-macros` and `use-macros` in Bootstrap for namespaces
33753482
;; that require their own macros
33763483
#?@(:cljs [[require-macros use-macros]
@@ -3392,9 +3499,9 @@
33923499
:use-macros use-macros
33933500
:require-macros require-macros
33943501
:rename-macros rename-macros
3395-
:uses uses
3502+
:uses (merge uses global-uses)
33963503
:requires requires
3397-
:renames (merge renames core-renames)
3504+
:renames (merge renames core-renames global-renames)
33983505
:imports imports}]
33993506
(swap! env/*compiler* update-in [::namespaces name] merge ns-info)
34003507
(merge {:op :ns
@@ -3434,6 +3541,7 @@
34343541
core-renames (reduce (fn [m [original renamed]]
34353542
(assoc m renamed (symbol "cljs.core" (str original))))
34363543
{} core-renames)
3544+
{global-uses :use global-renames :rename} (parse-global-refer-spec env args)
34373545
deps (atom [])
34383546
;; as-aliases can only be used *once* because they are about the reader
34393547
aliases (atom {:fns as-aliases :macros as-aliases})
@@ -3443,7 +3551,8 @@
34433551
(partial use->require env))
34443552
:use-macros (comp (partial parse-require-spec env true deps aliases)
34453553
(partial use->require env))
3446-
:import (partial parse-import-spec env deps)}
3554+
:import (partial parse-import-spec env deps)
3555+
:require-global #(parse-global-require-spec env env/*compiler* deps aliases %)}
34473556
reload (atom {:use nil :require nil :use-macros nil :require-macros nil})
34483557
reloads (atom {})
34493558
{uses :use requires :require renames :rename
@@ -3464,7 +3573,7 @@
34643573
(apply merge-with merge m
34653574
(map (spec-parsers k)
34663575
(remove #{:reload :reload-all} libs))))
3467-
{} (remove (fn [[r]] (= r :refer-clojure)) args))]
3576+
{} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args))]
34683577
(set! *cljs-ns* name)
34693578
(let [require-info
34703579
{:as-aliases as-aliases
@@ -3473,9 +3582,9 @@
34733582
:use-macros use-macros
34743583
:require-macros require-macros
34753584
:rename-macros rename-macros
3476-
:uses uses
3585+
:uses (merge uses global-uses)
34773586
:requires requires
3478-
:renames (merge renames core-renames)
3587+
:renames (merge renames core-renames global-renames)
34793588
:imports imports}]
34803589
(swap! env/*compiler* update-in [::namespaces name] merge-ns-info require-info env)
34813590
(merge {:op :ns*

src/main/clojure/cljs/closure.clj

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,13 +1127,17 @@
11271127
(let [requires (set (mapcat deps/-requires inputs))
11281128
required-js (js-dependencies opts requires)]
11291129
(concat
1130-
(map
1131-
(fn [{:keys [foreign url file provides requires] :as js-map}]
1132-
(let [url (or url (io/resource file))]
1133-
(merge
1134-
(javascript-file foreign url provides requires)
1135-
js-map)))
1136-
required-js)
1130+
(->> required-js
1131+
;; :foreign-libs which declare :external? have no sources (they are included
1132+
;; on the page via some script tag we'll never see). :require-global libs are
1133+
;; implicit :foreign-libs where :external? is true
1134+
(remove :external?)
1135+
(map
1136+
(fn [{:keys [foreign url file provides requires] :as js-map}]
1137+
(let [url (or url (io/resource file))]
1138+
(merge
1139+
(javascript-file foreign url provides requires)
1140+
js-map)))))
11371141
(when (-> @env/*compiler* :options :emit-constants)
11381142
[(constants-javascript-file opts)])
11391143
inputs)))
@@ -1604,7 +1608,11 @@
16041608
"], ["
16051609
;; even under Node.js where runtime require is possible
16061610
;; this is necessary - see CLJS-2151
1607-
(ns-list (cond->> (deps/-requires input)
1611+
(ns-list (cond->>
1612+
;; remove external? foreign deps - they are already loaded
1613+
;; in the environment, there is nothing to do.
1614+
;; :require-global is the typical case here
1615+
(remove ana/external-dep? (deps/-requires input))
16081616
;; under Node.js we emit native `require`s for these
16091617
(= :nodejs (:target opts))
16101618
(filter (complement ana/node-module-dep?))))

src/main/clojure/cljs/compiler.cljc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,10 @@
13671367
escape-string
13681368
wrap-in-double-quotes)
13691369
");"))
1370-
(emitln "goog.require('" (munge lib) "');"))))]
1370+
(if-not (ana/external-dep? lib)
1371+
(emitln "goog.require('" (munge lib) "');")
1372+
;; TODO: validate the lib exists
1373+
))))]
13711374
:cljs
13721375
[(and (ana/foreign-dep? lib)
13731376
(not (keyword-identical? optimizations :none)))

src/main/clojure/cljs/core.cljc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
defprotocol defrecord defstruct deftype delay destructure doseq dosync dotimes doto
1313
extend-protocol extend-type fn for future gen-class gen-interface
1414
if-let if-not import io! lazy-cat lazy-seq let letfn locking loop
15-
memfn ns or proxy proxy-super pvalues refer-clojure reify sync time
15+
memfn ns or proxy proxy-super pvalues reify sync time
1616
when when-first when-let when-not while with-bindings with-in-str
1717
with-loading-context with-local-vars with-open with-out-str with-precision with-redefs
1818
satisfies? identical? true? false? number? nil? instance? symbol? keyword? string? str get
@@ -3121,6 +3121,20 @@
31213121
[& args]
31223122
`(~'ns* ~(cons :refer-clojure args)))
31233123

3124+
(core/defmacro refer-global
3125+
"Refer global js vars. Supports renaming via :rename.
3126+
3127+
(refer-global :only '[Date Symbol] :rename '{Symbol Sym})"
3128+
[& args]
3129+
`(~'ns* ~(cons :refer-global args)))
3130+
3131+
(core/defmacro require-global
3132+
"Require libraries in the global JS environment.
3133+
3134+
(require-global '[SomeLib :as lib :refer [foo]])"
3135+
[& args]
3136+
`(~'ns* ~(cons :require-global args)))
3137+
31243138
;; INTERNAL - do not use, only for Node.js
31253139
(core/defmacro load-file* [f]
31263140
`(goog/nodeGlobalRequire ~f))

src/main/clojure/cljs/repl.cljc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,12 @@
256256
([repl-env requires]
257257
(load-dependencies repl-env requires nil))
258258
([repl-env requires opts]
259-
(doall (mapcat #(load-namespace repl-env % opts) (distinct requires)))))
259+
(->> requires
260+
distinct
261+
(remove ana/global-ns?)
262+
(remove ana/external-dep?)
263+
(mapcat #(load-namespace repl-env % opts))
264+
doall)))
260265

261266
(defn ^File js-src->cljs-src
262267
"Map a JavaScript output file back to the original ClojureScript source
@@ -652,7 +657,7 @@
652657
(defn- wrap-fn [form]
653658
(cond
654659
(and (seq? form)
655-
(#{'ns 'require 'require-macros
660+
(#{'ns 'require 'require-macros 'refer-global 'require-global
656661
'use 'use-macros 'import 'refer-clojure} (first form)))
657662
identity
658663

@@ -673,7 +678,7 @@
673678
(defn- init-wrap-fn [form]
674679
(cond
675680
(and (seq? form)
676-
(#{'ns 'require 'require-macros
681+
(#{'ns 'require 'require-macros 'refer-global
677682
'use 'use-macros 'import 'refer-clojure} (first form)))
678683
identity
679684

0 commit comments

Comments
 (0)