Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiled Elm code is not friendly to JS modules #1029

Closed
rtfeldman opened this issue Aug 26, 2015 · 5 comments
Closed

Compiled Elm code is not friendly to JS modules #1029

rtfeldman opened this issue Aug 26, 2015 · 5 comments

Comments

@rtfeldman
Copy link
Member

Current JS best practices have abandoned the 1990s-era practice of slapping another <script> tag on the page, expecting that script to mutate the global window object, and moving on. Today, the best practice is to use a module system (e.g. RequireJS, AMD, ES6 modules) and a build tool (e.g. browserify, webpack) to combine them.

At the moment, compiled Elm code supports only the 90s approach: add Elm to the global object and move on. As such, there is no way to (for example) require an Elm module from within Node, short of an extra wrapping step. The best practice to avoid this is to add a snippet of boilerplate which detects the JS module environment and proceeds appropriately. Here is an example from a popular JS library.

This would enable the following, in either Node or in the browser (when using something like Browserify):

var Elm = require("scripts/elm.js");

...as opposed to the current "add it to the page and expect the Global to be there."

Concretely, this would mean adding the following boilerplate (adapted from the aforementioned Q snippet) to compiled Elm output:

 (function (definition) {
    "use strict";

    // This file will function properly as a <script> tag, or a module
    // using CommonJS and NodeJS or RequireJS module formats.  In
    // Common/Node/RequireJS, the module exports the Elm API and when
    // executed as a simple <script>, it creates an Elm global instead.

    // Montage Require
    if (typeof bootstrap === "function") {
        bootstrap("promise", definition);

    // CommonJS
    } else if (typeof exports === "object" && typeof module === "object") {
        module.exports = definition();

    // RequireJS
    } else if (typeof define === "function" && define.amd) {
        define(definition);

    // SES (Secure EcmaScript)
    } else if (typeof ses !== "undefined") {
        if (!ses.ok()) {
            return;
        } else {
            ses.makeElm = definition;
        }

    // <script>
    } else if (typeof window !== "undefined" || typeof self !== "undefined") {
        // Prefer window over self for add-on scripts. Use self for
        // non-windowed contexts.
        var global = typeof window !== "undefined" ? window : self;

        // Get the `window` object, save the previous Elm global
        // and initialize Elm as a global.
        var previousElm = global.Elm;
        global.Elm = definition();

        // Add a noConflict function so Elm can be removed from the
        // global namespace.
        global.Elm.noConflict = function () {
            global.Elm = previousElm;
            return this;
        };

    } else {
        throw new Error("This environment was not anticipated by Elm. Please file a bug at https://github.com/elm-lang/elm-compiler/issues");
    }

})(function () {
  var Elm = {};

  // Current emitted code goes here...

  return Elm;
});

This might seem like a lot, but:

  1. Minified, it adds about 0.6 KB. Considering this is the best practice by popular JS libraries, that seems to be a consensus acceptable overhead.
  2. It now means your Elm code "just works" in best-practice JS, in both the browser and on Node.
  3. When transitioning from an existing JS codebase to Elm, you can now introduce multiple Elm modules to the same page, piecemeal, by calling require multiple times. Each will be namespaced separately with no conflicts.
@rgrempel
Copy link

I think that this would be a very valuable approach to adopt.

@mgold
Copy link
Contributor

mgold commented Aug 26, 2015

Seems reasonable. Can this be done as part of the larger codegen overhaul?

@laszlopandy
Copy link
Contributor

Wow. I don't know what SES is, or why we need the noConflict function.
I have seen a much simpler version used by Bacon.js which handles Node, RequireJS and script tag with just a few lines:
https://github.com/baconjs/bacon.js/blob/master/dist/Bacon.js#L3424

@rtfeldman
Copy link
Member Author

noConflict is definitely not necessary. (In case it wasn't clear, it lets you include by adding <script> to a page that is already using a global called Elm without overriding that global.)

A trimmed-down implementation like Bacon's seems fine. I'd say the main thing is that Node, CommonJS, AMD, and <script> all "just work."

@evancz
Copy link
Member

evancz commented Aug 27, 2015

Can we close this down and open an issue on elm-make just outlining the smaller recommendation and linking here. When Joey and I get to this, we will use that snippet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants