Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0c3c41
wip
swannodette Jul 9, 2025
dfe9c65
- parse-global-refer-spec is more like parse-ns-excludes
swannodette Jul 15, 2025
c2487b8
- fix parse-global-refer-spec validation
swannodette Jul 25, 2025
c47146a
- refactor validation
swannodette Jul 25, 2025
c63f305
- works for Date in a Node REPL
swannodette Jul 25, 2025
34690cc
- :refer-global is not a normal spec-parser
swannodette Jul 25, 2025
688a216
- refer-clojure exclude duped
swannodette Jul 25, 2025
fdb9c7a
- add case to missing-use for global referred vars
swannodette Jul 25, 2025
53abd2c
- missing binding for test
swannodette Jul 25, 2025
dd85919
- fix load-dependencies so we drop global namespaces
swannodette Jul 29, 2025
f4b6f56
- move global-ns? predicate to analyzer
swannodette Jul 29, 2025
7caab4a
Merge branch 'master' into cljs-3233/globals
swannodette Oct 4, 2025
3e50ebe
:rename symbols must first occur in :only, fix check & tests
swannodette Oct 5, 2025
1ce164f
copy over basic shape of parse-global-require-spec
swannodette Oct 7, 2025
58ac381
global requires are not "real" namespace deps, don't add it to the list
swannodette Oct 9, 2025
a96edfe
need js/ qualify global libs, add parsing test
swannodette Oct 9, 2025
0564ab4
typo, update test - add :rename case
swannodette Oct 9, 2025
a5178ca
uncomment test-refer-global test
swannodette Oct 9, 2025
cc972ab
update string check for ns spec test
swannodette Oct 9, 2025
e27c1da
js-lib -> global-lib, less clashing w/ existing use of js-lib
swannodette Oct 9, 2025
389adac
require-global test
swannodette Oct 9, 2025
40caf64
:require-global
swannodette Oct 10, 2025
221d8a4
stuff missing from last commit
swannodette Oct 10, 2025
c5e1fea
we need to filter external deps from cljs_deps.js
swannodette Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 126 additions & 17 deletions src/main/clojure/cljs/analyzer.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -2781,6 +2781,17 @@
(if (and (.exists cljcf) (.isFile cljcf))
cljcf))))))

(defn external-dep?
"Returns true if the library is an :external? foreign dep. This means no source is provided
for the library, i.e. it will be provided by some script tag on the page, or loaded by some
other means into the JS execution environment."
#?(:cljs {:tag boolean})
[dep]
(let [js-index (:js-dependency-index @env/*compiler*)]
(if-some [[_ {:keys [foreign external?]}] (find js-index (name (-> dep lib&sublib first)))]
(and foreign external?)
false)))

(defn foreign-dep?
#?(:cljs {:tag boolean})
[dep]
Expand Down Expand Up @@ -2828,13 +2839,19 @@
(error env
(error-message :undeclared-ns {:ns-sym dep :js-provide (name dep)}))))))))))))

(defn global-ns? [x]
(or (= 'js x)
(= "js" (namespace x))))

(defn missing-use? [lib sym cenv]
(let [js-lib (get-in cenv [:js-dependency-index (name lib)])]
(and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found)
(not (= (get js-lib :group) :goog))
(not (get js-lib :closure-lib))
(not (node-module-dep? lib))
(not (dep-has-global-exports? lib)))))
;; ignore globals referred via :refer-global
(when-not (global-ns? lib)
(let [js-lib (get-in cenv [:js-dependency-index (name lib)])]
(and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found)
(not (= (get js-lib :group) :goog))
(not (get js-lib :closure-lib))
(not (node-module-dep? lib))
(not (dep-has-global-exports? lib))))))

(defn missing-rename? [sym cenv]
(let [lib (symbol (namespace sym))
Expand Down Expand Up @@ -3047,6 +3064,90 @@
ret
(recur fs ret true)))))

(defn parse-global-refer-spec
[env args]
(let [xs (filter #(-> % first (= :refer-global)) args)
cnt (count xs)]
(cond
(> cnt 1)
(throw (error env "Only one :refer-global form is allowed per namespace definition"))

(== cnt 1)
(let [[_ & {:keys [only rename] :as parsed-spec}] (first xs)
only-set (set only)
err-str "Only (:refer-global :only [names]) and optionally `:rename {from to}` specs supported.
:rename symbols must be present in :only"]
(when-not (or (empty? only)
(and (vector? only)
(every? symbol only)))
(throw (error env err-str)))
(when-not (or (empty? rename)
(and (map? rename)
(every? symbol (mapcat identity rename))
(every? only-set (keys rename))))
(throw (error env (str err-str (pr-str parsed-spec)))))
(when-not (every? #{:only :rename} (keys parsed-spec))
(throw (error env (str err-str (pr-str parsed-spec)))))
{:use (zipmap only (repeat 'js))
:rename (into {}
(map (fn [[orig new-name]]
[new-name (symbol "js" (str orig))]))
rename)}))))

(defn parse-global-require-spec
[env cenv deps aliases spec]
(if (or (symbol? spec) (string? spec))
(recur env cenv deps aliases [spec])
(do
(basic-validate-ns-spec env false spec)
(let [[lib & opts] spec
{alias :as referred :refer renamed :rename
:or {alias (if (string? lib)
(symbol (munge lib))
lib)}}
(apply hash-map opts)
referred-without-renamed (seq (remove (set (keys renamed)) referred))
[rk uk renk] [:require :use :rename]]
(when-not (or (symbol? alias) (nil? alias))
(throw
(error env
(parse-ns-error-msg spec
":as must be followed by a symbol in :require / :require-macros"))))
(when (some? alias)
(let [lib' ((:fns @aliases) alias)]
(when (and (some? lib') (not= lib lib'))
(throw (error env (parse-ns-error-msg spec ":as alias must be unique"))))
(when (= alias 'js)
(when-not (= lib (get-in @aliases [:fns 'js])) ; warn only once
(warning :js-used-as-alias env {:spec spec})))
(swap! aliases update-in [:fns] conj [alias lib])))
(when-not (or (and (sequential? referred)
(every? symbol? referred))
(nil? referred))
(throw
(error env
(parse-ns-error-msg spec
":refer must be followed by a sequence of symbols in :require / :require-macros"))))
(swap! deps conj lib)
(let [ret (merge
(when (some? alias)
{rk (merge {alias lib} {lib lib})})
(when (some? referred-without-renamed)
{uk (apply hash-map (interleave referred-without-renamed (repeat lib)))})
(when (some? renamed)
{renk (reduce (fn [m [original renamed]]
(when-not (some #{original} referred)
(throw (error env
(str "Renamed symbol " original " not referred"))))
(assoc m renamed (symbol (str lib) (str original))))
{} renamed)}))]
(swap! cenv assoc-in [:js-dependency-index (str lib)]
{:external? true
:foreign true
:provides [(str lib)]
:global-exports {lib lib}})
ret)))))

(defn parse-require-spec [env macros? deps aliases spec]
(if (or (symbol? spec) (string? spec))
(recur env macros? deps aliases [spec])
Expand Down Expand Up @@ -3300,6 +3401,10 @@
(select-keys new deep-merge-keys))))
new))

(def ns-spec-cases
#{:use :use-macros :require :require-macros
:import :refer-global :require-global})

(defmethod parse 'ns
[_ env [_ name & args :as form] _ opts]
(when-not *allow-ns*
Expand Down Expand Up @@ -3334,6 +3439,7 @@
core-renames (reduce (fn [m [original renamed]]
(assoc m renamed (symbol "cljs.core" (str original))))
{} core-renames)
{global-uses :use global-renames :rename} (parse-global-refer-spec env args)
deps (atom [])
;; as-aliases can only be used *once* because they are about the reader
aliases (atom {:fns as-aliases :macros as-aliases})
Expand All @@ -3343,17 +3449,18 @@
(partial use->require env))
:use-macros (comp (partial parse-require-spec env true deps aliases)
(partial use->require env))
:import (partial parse-import-spec env deps)}
valid-forms (atom #{:use :use-macros :require :require-macros :import})
:import (partial parse-import-spec env deps)
:require-global #(parse-global-require-spec env env/*compiler* deps aliases %)}
valid-forms (atom #{:use :use-macros :require :require-macros :require-global :import})
reload (atom {:use nil :require nil :use-macros nil :require-macros nil})
reloads (atom {})
{uses :use requires :require renames :rename
use-macros :use-macros require-macros :require-macros
rename-macros :rename-macros imports :import :as params}
(reduce
(fn [m [k & libs :as libspec]]
(when-not (#{:use :use-macros :require :require-macros :import} k)
(throw (error env (str "Only :refer-clojure, :require, :require-macros, :use, :use-macros, and :import libspecs supported. Got " libspec " instead."))))
(when-not (#{:use :use-macros :require :require-macros :require-global :import} k)
(throw (error env (str "Only :refer-clojure, :require, :require-macros, :use, :use-macros, :require-global and :import libspecs supported. Got " libspec " instead."))))
(when-not (@valid-forms k)
(throw (error env (str "Only one " k " form is allowed per namespace definition"))))
(swap! valid-forms disj k)
Expand All @@ -3370,7 +3477,7 @@
(apply merge-with merge m
(map (spec-parsers k)
(remove #{:reload :reload-all} libs))))
{} (remove (fn [[r]] (= r :refer-clojure)) args))
{} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args))
;; patch `require-macros` and `use-macros` in Bootstrap for namespaces
;; that require their own macros
#?@(:cljs [[require-macros use-macros]
Expand All @@ -3392,9 +3499,9 @@
:use-macros use-macros
:require-macros require-macros
:rename-macros rename-macros
:uses uses
:uses (merge uses global-uses)
:requires requires
:renames (merge renames core-renames)
:renames (merge renames core-renames global-renames)
:imports imports}]
(swap! env/*compiler* update-in [::namespaces name] merge ns-info)
(merge {:op :ns
Expand Down Expand Up @@ -3434,6 +3541,7 @@
core-renames (reduce (fn [m [original renamed]]
(assoc m renamed (symbol "cljs.core" (str original))))
{} core-renames)
{global-uses :use global-renames :rename} (parse-global-refer-spec env args)
deps (atom [])
;; as-aliases can only be used *once* because they are about the reader
aliases (atom {:fns as-aliases :macros as-aliases})
Expand All @@ -3443,7 +3551,8 @@
(partial use->require env))
:use-macros (comp (partial parse-require-spec env true deps aliases)
(partial use->require env))
:import (partial parse-import-spec env deps)}
:import (partial parse-import-spec env deps)
:require-global #(parse-global-require-spec env env/*compiler* deps aliases %)}
reload (atom {:use nil :require nil :use-macros nil :require-macros nil})
reloads (atom {})
{uses :use requires :require renames :rename
Expand All @@ -3464,7 +3573,7 @@
(apply merge-with merge m
(map (spec-parsers k)
(remove #{:reload :reload-all} libs))))
{} (remove (fn [[r]] (= r :refer-clojure)) args))]
{} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args))]
(set! *cljs-ns* name)
(let [require-info
{:as-aliases as-aliases
Expand All @@ -3473,9 +3582,9 @@
:use-macros use-macros
:require-macros require-macros
:rename-macros rename-macros
:uses uses
:uses (merge uses global-uses)
:requires requires
:renames (merge renames core-renames)
:renames (merge renames core-renames global-renames)
:imports imports}]
(swap! env/*compiler* update-in [::namespaces name] merge-ns-info require-info env)
(merge {:op :ns*
Expand Down
24 changes: 16 additions & 8 deletions src/main/clojure/cljs/closure.clj
Original file line number Diff line number Diff line change
Expand Up @@ -1127,13 +1127,17 @@
(let [requires (set (mapcat deps/-requires inputs))
required-js (js-dependencies opts requires)]
(concat
(map
(fn [{:keys [foreign url file provides requires] :as js-map}]
(let [url (or url (io/resource file))]
(merge
(javascript-file foreign url provides requires)
js-map)))
required-js)
(->> required-js
;; :foreign-libs which declare :external? have no sources (they are included
;; on the page via some script tag we'll never see). :require-global libs are
;; implicit :foreign-libs where :external? is true
(remove :external?)
(map
(fn [{:keys [foreign url file provides requires] :as js-map}]
(let [url (or url (io/resource file))]
(merge
(javascript-file foreign url provides requires)
js-map)))))
(when (-> @env/*compiler* :options :emit-constants)
[(constants-javascript-file opts)])
inputs)))
Expand Down Expand Up @@ -1604,7 +1608,11 @@
"], ["
;; even under Node.js where runtime require is possible
;; this is necessary - see CLJS-2151
(ns-list (cond->> (deps/-requires input)
(ns-list (cond->>
;; remove external? foreign deps - they are already loaded
;; in the environment, there is nothing to do.
;; :require-global is the typical case here
(remove ana/external-dep? (deps/-requires input))
;; under Node.js we emit native `require`s for these
(= :nodejs (:target opts))
(filter (complement ana/node-module-dep?))))
Expand Down
5 changes: 4 additions & 1 deletion src/main/clojure/cljs/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,10 @@
escape-string
wrap-in-double-quotes)
");"))
(emitln "goog.require('" (munge lib) "');"))))]
(if-not (ana/external-dep? lib)
(emitln "goog.require('" (munge lib) "');")
;; TODO: validate the lib exists
))))]
:cljs
[(and (ana/foreign-dep? lib)
(not (keyword-identical? optimizations :none)))
Expand Down
16 changes: 15 additions & 1 deletion src/main/clojure/cljs/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
defprotocol defrecord defstruct deftype delay destructure doseq dosync dotimes doto
extend-protocol extend-type fn for future gen-class gen-interface
if-let if-not import io! lazy-cat lazy-seq let letfn locking loop
memfn ns or proxy proxy-super pvalues refer-clojure reify sync time
memfn ns or proxy proxy-super pvalues reify sync time
when when-first when-let when-not while with-bindings with-in-str
with-loading-context with-local-vars with-open with-out-str with-precision with-redefs
satisfies? identical? true? false? number? nil? instance? symbol? keyword? string? str get
Expand Down Expand Up @@ -3121,6 +3121,20 @@
[& args]
`(~'ns* ~(cons :refer-clojure args)))

(core/defmacro refer-global
"Refer global js vars. Supports renaming via :rename.

(refer-global :only '[Date Symbol] :rename '{Symbol Sym})"
[& args]
`(~'ns* ~(cons :refer-global args)))

(core/defmacro require-global
"Require libraries in the global JS environment.

(require-global '[SomeLib :as lib :refer [foo]])"
[& args]
`(~'ns* ~(cons :require-global args)))

;; INTERNAL - do not use, only for Node.js
(core/defmacro load-file* [f]
`(goog/nodeGlobalRequire ~f))
Expand Down
11 changes: 8 additions & 3 deletions src/main/clojure/cljs/repl.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,12 @@
([repl-env requires]
(load-dependencies repl-env requires nil))
([repl-env requires opts]
(doall (mapcat #(load-namespace repl-env % opts) (distinct requires)))))
(->> requires
distinct
(remove ana/global-ns?)
(remove ana/external-dep?)
(mapcat #(load-namespace repl-env % opts))
doall)))

(defn ^File js-src->cljs-src
"Map a JavaScript output file back to the original ClojureScript source
Expand Down Expand Up @@ -652,7 +657,7 @@
(defn- wrap-fn [form]
(cond
(and (seq? form)
(#{'ns 'require 'require-macros
(#{'ns 'require 'require-macros 'refer-global 'require-global
'use 'use-macros 'import 'refer-clojure} (first form)))
identity

Expand All @@ -673,7 +678,7 @@
(defn- init-wrap-fn [form]
(cond
(and (seq? form)
(#{'ns 'require 'require-macros
(#{'ns 'require 'require-macros 'refer-global
'use 'use-macros 'import 'refer-clojure} (first form)))
identity

Expand Down
Loading