diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index ceebbe973..924ac3477 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -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] @@ -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)) @@ -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]) @@ -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* @@ -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}) @@ -3343,8 +3449,9 @@ (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 @@ -3352,8 +3459,8 @@ 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) @@ -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] @@ -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 @@ -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}) @@ -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 @@ -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 @@ -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* diff --git a/src/main/clojure/cljs/closure.clj b/src/main/clojure/cljs/closure.clj index 550fd68d5..2c923d85c 100644 --- a/src/main/clojure/cljs/closure.clj +++ b/src/main/clojure/cljs/closure.clj @@ -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))) @@ -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?)))) diff --git a/src/main/clojure/cljs/compiler.cljc b/src/main/clojure/cljs/compiler.cljc index 522a88357..3e0d42480 100644 --- a/src/main/clojure/cljs/compiler.cljc +++ b/src/main/clojure/cljs/compiler.cljc @@ -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))) diff --git a/src/main/clojure/cljs/core.cljc b/src/main/clojure/cljs/core.cljc index b326a3fa6..a58ec944f 100644 --- a/src/main/clojure/cljs/core.cljc +++ b/src/main/clojure/cljs/core.cljc @@ -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 @@ -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)) diff --git a/src/main/clojure/cljs/repl.cljc b/src/main/clojure/cljs/repl.cljc index b685a62e5..85117c725 100644 --- a/src/main/clojure/cljs/repl.cljc +++ b/src/main/clojure/cljs/repl.cljc @@ -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 @@ -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 @@ -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 diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index f1b639938..ca388182d 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -171,7 +171,7 @@ (analyze ns-env '(ns foo.bar (:unless []))) (catch Exception e (.getMessage (.getCause e)))) - "Only :refer-clojure, :require, :require-macros, :use, :use-macros, and :import libspecs supported. Got (:unless []) instead.")) + "Only :refer-clojure, :require, :require-macros, :use, :use-macros, :require-global and :import libspecs supported. Got (:unless []) instead.")) (is (.startsWith (try (analyze ns-env '(ns foo.bar (:require baz.woz) (:require noz.goz))) @@ -387,6 +387,32 @@ :renames {map clj-map}})) (is (set? (:excludes parsed))))) +(deftest test-parse-global-refer + (let [parsed (ana/parse-global-refer-spec {} + '((:refer-global :only [Date Symbol] :rename {Symbol JSSymbol})))] + (is (= parsed + '{:use {Date js Symbol js} + :rename {JSSymbol js/Symbol}})))) + +(deftest test-parse-require-global + (let [cenv (atom {}) + deps (atom []) + parsed (ana/parse-global-require-spec {} cenv deps (atom {:fns {}}) + '[React :refer [createElement] :as react])] + (println (pr-str @cenv) (pr-str @deps)) + (is (= parsed + '{:require {react React + React React} + :use {createElement React}}))) + (let [cenv (atom {}) + deps (atom []) + parsed (ana/parse-global-require-spec {} cenv deps (atom {:fns {}}) + '[React :refer [createElement] :rename {createElement create} :as react])] + (is (= parsed + '{:require {react React + React React} + :rename {create React/createElement}})))) + (deftest test-cljs-1785-js-shadowed-by-local (let [ws (atom [])] (ana/with-warning-handlers [(collecting-warning-handler ws)] @@ -547,6 +573,14 @@ (analyze test-env '(map #(require '[clojure.set :as set]) [1 2])))))) +(deftest test-analyze-refer-global + (testing "refer-global macro expr return expected AST" + (binding [ana/*cljs-ns* ana/*cljs-ns* + ana/*cljs-warnings* nil] + (let [test-env (ana/empty-env)] + (is (= (-> (analyze test-env '(refer-global :only '[Date])) :uses vals set) + '#{js})))))) + (deftest test-gen-user-ns ;; note: can't use `with-redefs` because direct-linking is enabled (let [s "src/cljs/foo.cljs" @@ -1533,3 +1567,27 @@ (ana/gen-constant-id '+))) (is (not= (ana/gen-constant-id 'foo.bar) (ana/gen-constant-id 'foo$bar)))) + +;; ----------------------------------------------------------------------------- +;; :refer-global / :require-global ns parsing tests + +(deftest test-refer-global + (binding [ana/*cljs-ns* ana/*cljs-ns*] + (let [parsed-ns (env/with-compiler-env test-cenv + (analyze test-env + '(ns foo.core + (:refer-global :only [Date] :rename {Date MyDate}))))] + (= (:renames parsed-ns) + '{MyDate js/Date})))) + +(deftest test-require-global + (binding [ana/*cljs-ns* ana/*cljs-ns*] + (let [parsed-ns (env/with-compiler-env test-cenv + (analyze test-env + '(ns foo.core + (:require-global [React :as react :refer [createElement]]))))] + (is (= (:requires parsed-ns) + '{React React + react React})) + (is (= (:uses parsed-ns) + '{createElement React})))))