Skip to content

Instantly share code, notes, and snippets.

@Deraen
Last active July 10, 2017 22:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Deraen/7a8f5ffef89c50b0cbe8df5d6b4b98eb to your computer and use it in GitHub Desktop.
Save Deraen/7a8f5ffef89c50b0cbe8df5d6b4b98eb to your computer and use it in GitHub Desktop.
WIP: Cljs foreign-lib requires brain dump

Problem

Currently libraries need to write some logic to load JS libraries from various places. For example Reagent supports:

  1. (Default) Cljsjs packages which export js/React, js/ReactDOM, js/ReactDOMServer and js/createReactClass
  2. Npm packages, with require when using Node target

Because the default is Cljsjs, Reagent namespaces depend on cljsjs namespaces like cljsjs.react. This means that to use Reagent with option 2. requires creating empty stub namespaces that provides these.

Using Reagent with :npm-deps is not possible, because... FIXME

(defn module []
  (cond
    (some? imported) imported
    (exists? js/ReactDOM) (set! imported js/ReactDOM)
    (exists? js/require) (or (set! imported (js/require "react-dom"))
                             (throw (js/Error. "require('react-dom') failed")))
    :else
    (throw (js/Error. "js/ReactDOM is missing"))))

Future

Next version of Cljs will support string requires, https://dev.clojure.org/jira/browse/CLJS-2061. These are currently only useful together with :npm-deps. String requires exists because some Node packages need to be referred using names which can't be normally expressed, e.g. react-dom/server, where / is illegal character in symbols.

But string requires don't need to be tied to :npm-deps feature. String requires are only about making it possible to write these requires. The dependency loading should work no matter how these modules are provided: :npm-deps, or Cljsjs style :foreign-lib (which provides browser-ready JS files)..

It will take for everyone to adapt :npm-deps and this will be even slower when users will only use this is libraries use it. But libraries will have hard time using this if this breaks the existing configurations, which use e.g. Cljsjs.

:npm-deps modules are converted to Closure modules and loaded as such. These modules get names like module$absolute$file$path$react. Cljs compiler takes care of mapping requires of e.g. react into this module.

Solution

Foreign-lib code can't be loaded as Closure modules. But is possible to create a simple wrapper Closure module:

goog.provide("module$react");
module$react = window.React;

Which creates valid Closure module. This module can be added to Cljs dependency index and can be required like :npm-deps (normally, or string requires when needed).

To select the window global object which provides the lib, new configuration param can be added to foreign-lib map: :js-global "React".

Implementation

Some minimal changes are required to allow strings in requires for foreign-libs.

After/before processing JS-modules using CJS/ES6, the foreign-libs containing :js-global property are selected, and for each a wrapper Closure module is created.

  1. Foreign lib with {:file "react-dom-server.js" :provides ["react-dom/server"] :requires ["react"] :js-global "ReactDOMServer"]}
  2. module$react$dom$server.js file is created based on foreign-lib map
  3. react-dom/server -> module$react$dom$server is added to dependency index
  4. The created Closure lib is loaded later, by closure/js-dependencies
    • At this point, goog.requires/provide from the file are read and used to select dependencies etc.
    • module$react$dom$server has to depend on the original foreign-lib so that is also added to build d dependencies by Cljs and loaded
    • Probably the original foreign-lib should be renamed, e.g. module$react$dom$server$original
  • This works for both cases where Npm module exports an object, or an function
    • Npm modules always export single object or function, so :js-global should be enough for this use
    • What about ES6 modules?
    • No need to support browser libraries that are not ever used through :npm-deps? These might export several objects.
  • This will work correctly with advanced builds also.
    • Might be some minimal difference in output size?

Alternatives?

  • Would it be possible for Cljsjs/user to write these wrapper modules themselves?
    • Perhaps, but quite hard. There needs to be mapping from e.g. react-dom/server -> what ever is the module name, so there would need to be documented way to name these files.
    • Closure modules can't currently requires Foreign-libs, which means that the real JS code would not be included

Next steps

More notes

  • On Node, similar wrapper Closure module could be created to allow libraries work both with Node and on browser with :npm-deps: module$react$dom$server = requires("react-dom/server");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment