Sunday, August 14, 2022
HomeWordPress DevelopmentJS MODULE LOADERS; or, a short journey by means of hell

JS MODULE LOADERS; or, a short journey by means of hell




Introduction

There is a saying in protection circles: “amateurs discuss technique; professionals discuss logistics”. In different phrases, what looks as if probably the most mundane aspect of advanced engineering duties (transferring stuff on time from level A to level B) is a surprisingly crucial aspect of success.

If I needed to pressure an analogy right here, I would say for the developer group that “amateurs discuss code, professionals discuss integration”. It seems that writing code (particularly from scratch) is surprisingly simple, whereas placing code collectively (particularly code you did not write your self) is surprisingly tough.

So, on the earth of JavaScript, how can we put code collectively? Nicely, it relies upon. Within the 12 months of our lord two-thousand and twenty-two, 26 years after JavaScript was launched, we nonetheless do not have a constant method to combine items of code collectively. We do not also have a constant method to outline what these items of code are!



The Issues

You will be aware the phrase “constant”, although. There are a lot of methods you might go about it, however few methods which are really interoperable. Let’s break this into three particular issues:

  1. How are packages managed?

  2. How are modules exported?

  3. How are modules specified?

For instance, the reply to #1 may very well be NPM, Yarn, or some type of CDN. It may be so simple as git submodules. (For causes I will not dive too deeply into, I choose the latter method, particularly as a result of it’s fully decoupled from the module you might be developing–and even the language you might be growing in.)

The reply to #2 may very well be one thing like AMD/RequireJS modules, or CommonJS/Node, or browser-level script tags inside a worldwide scope (yuck!). In fact, Browserify or WebPack might aid you right here in the event you’re actually a giant fan of the latter. I am a giant fan of AMD/RequireJS however there is no arguing that with the ability to run (and check) a codebase from the command line (domestically or remotely) is HUGELY advantageous, each for improvement (simply messing round) and for deployment (e.g., automated testing from a CI job).

The reply to #3 is a bit more refined, in no small half as a result of with one thing like CommonJS/Node it is solely implicit. With AMD/RequireJS, you will have particular “require”, “exports”, and “module” parameters to a “outline()” perform. These exist in CommonJS/Node, too, however they’re implied. Strive printing “module” to console.log someday and have a look at all of the juicy particulars you have been lacking.



SFJMs and UMD

However this does not embrace the contents of your bundle.json (if any) and even with AMD/RequireJS there is no particular commonplace for attaching metadata and different module properties. That is one purpose I put collectively the SFJM commonplace in a earlier dev.to article:

https://dev.to/tythos/single-file-javascript-modules-7aj

However no matter your method, the module loader (e.g., export downside outlined in #2 above) goes to be sticky. That is one purpose the UMD commonplace has emerged, for which there’s a superb writeup by Jim Fischer:

https://jameshfisher.com/2020/10/04/what-are-umd-modules/

UMD specifies a header to be pasted in entrance of your define-like closure. It is utilized by a couple of main libraries, together with help for sure construct configurations, like THREE.js:

https://github.com/mrdoob/three.js/blob/dev/construct/three.js



The Header

The UMD header has a number of variations however we’ll think about the next one from Jim Fischer’s writeup:

// myModuleName.js
(perform (root, manufacturing facility) {
    if (typeof outline === 'perform' && outline.amd) {
        // AMD. Register as an nameless module.
        outline(['exports', 'b'], manufacturing facility);
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        // CommonJS
        manufacturing facility(exports, require('b'));
    } else {
        // Browser globals
        manufacturing facility((root.myModuleName = {}), root.b);
    }
}(typeof self !== 'undefined' ? self : this, perform (exports, b) {
    // Use b in some style.

    // connect properties to the exports object to outline
    // the exported module properties.
    exports.motion = perform () {};
}));
Enter fullscreen mode

Exit fullscreen mode

There are successfully three use instances captured right here: AMD/RequireJS; CommonJS/Node; and browser globals. Let’s be sincere, though–it’s ugly. (This is not a hack at Jim, this can be a normal UMD downside.) Amongst different issues, this is what bugs me:

  • It is simply plain bulky–that’s quite a lot of textual content to stick on the high of each module

  • It truly tries too hard–I’ve by no means discovered a must help browser globals, I simply want my AMD/RequireJS-based single-file JavaScript modules to have the ability to run/check in a CommonJS/Node atmosphere

  • The dependency listings are explicitly tied into the header–so it is not truly reusable. It’s important to customise it for each module! Examine this to easily specifying const b = require('b'); inside the closure manufacturing facility itself and clearly there is a huge distinction.

  • I am not considering treating usecases equally. I am writing in AMD/RequireJS, and capturing CommonJS/Node loading is the sting case.

The principle downside right here with the final level is, AMD/RequireJS already give us a really clear closure and explicitly module definition interface. It is CommonJS/Node that require the hack. So, can we streamline the header and concentrate on adapting the latter to the previous? Ideally in a method that’s agnostic to dependencies? Nicely, since I am writing this text, you possibly can in all probability inform the reply is “sure”.



My Strategy

Let’s begin with symbols. What’s out there, and what is not? Let’s begin with a AMD/RequireJS module already outlined and dealing. In the event you put your self within the thoughts of the CommonJS/Node interpreter, the very first thing you may notice is that, whereas “require”, “exports”, and “module” are already outlined implicitly, the “outline” manufacturing facility just isn’t. So, that is the basis of our downside: we have to outline a “outline” (ha ha) manufacturing facility that guides CommonJS/Node to interpret the module definition closure in a constant method.

There is a good instance of the conditional for this from UMD that we will borrow (and modify barely):

if (typeof(outline) !== "perform" || outline.amd !== true) {
Enter fullscreen mode

Exit fullscreen mode

Apparently, you possibly can’t simply verify to see if outline exists. You have to make certain it does not truly exist AS THE AMD IMPLEMENTATION, as a result of CommonJS/Node could retain the “outline” image exterior of this context–for instance, within the scope of one other module that’s “require()”-ing this one. Weird, however true.

So, now our purpose is to outline “outline()”. How can this be tailored to a CommonJS/Node scope? What we have to guarantee is, the existence of an an identical “outline()” interface:

  • It ought to take a single parameter, an nameless perform (which we’ll name the “manufacturing facility” right here) inside whose closure the module contents are outlined.

  • That perform ought to have the next interface: “require” (a perform that resolves/returns any module dependencies primarily based on path); “exports” (an Object that defines what symbols will likely be out there to exterior modules); and “module” (a definition of module properties that features “module.exports”, which factors to “exports”.

  • Outline ought to name that perform and return the export symbols of the module. (Within the case of a SFJM-compatible definition, this can even embrace bundle.json-like module metadata, together with a map of dependencies.)

The final level is attention-grabbing as a result of a) there’s already a number of references to the module exports, and b) even AMD/RequireJS helps a number of/non-compulsory routes for export symbols. And this is without doubt one of the stickiest points on the coronary heart of cross-compatibility: the “exports” image can persist and be incorrectly mapped by CommonJS/Node if not explicitly returned!



Thanks, Exports, You are The Actual (factor stopping us from reaching) MVP

Jesus, what a nightmare. For that reason, we’re going to modify how our manufacturing facility closure works:

  • We’re going to explicitly “disable” the “exports” parameter by passing an empty Object (“{}”) because the second parameter to the manufacturing facility.

  • We’re going to explicitly return the module exports from the manufacturing facility implementation

  • We’re going to explicitly map the outcomes of the manufacturing facility name to the (file-level) “module.exports” property.

The mix of those changes signifies that, whereas AMD/RequireJS helps a number of routes, we’re going to constrain our module implementations to explicitly returning export symbols from the manufacturing facility name to route them to the right CommonJS/Node image.

In the event you do not do this–and I misplaced some hair debugging this–you find yourself with a really “attention-grabbing” (learn: batshit insane in solely the way in which CommonJS/Node might be) bug through which the mother or father module (require()’ing a dependency module) will get “wires crossed” and has export symbols persist between scopes.

It is weird, significantly as a result of it ONLY HAPPENS OUTSIDE THE REPL! So, you possibly can run equal module strategies from the REPL and so they’re fine–but attempting to map it inside the module itself (after which, say, calling it from the command line) will break each time.

So, what does this seem like, virtually? It means the “outline” definition we’re placing into the conditional we wrote above appears one thing like this:

outline = (manufacturing facility) => module.exports = manufacturing facility(require, {}, module);
Enter fullscreen mode

Exit fullscreen mode

It additionally means our module closure begins with explicitly disabling the “exports” image so poor outdated CommonJS/Node does not get wires crossed:

outline(perform(require, _, module) {
    let exports = {};
Enter fullscreen mode

Exit fullscreen mode

Sigh. Some day it should all make sense. However then it will not be JavaScript. 😉



Examples

What does this seem like “within the wild”, then? Here is a GitHub undertaking that gives a fairly clear instance:

https://github.com/Tythos/umd-light/

A short tour:

  • “index.js” reveals how the entry level might be wrapped in the identical closure that makes use of the “require()” name to transparently load the dependency

  • “index.js” additionally reveals us how one can add a SFJM-style hook for (from CommonJS/Node) working an entry level (“most important“) ought to this module be known as from the command line

  • “.gitmodules” tells us that the dependency is managed as a submodule

  • “lib/” accommodates the submodules we use

  • “lib/jtx” is the particular submodule reference (remember to submodule-init and submodule-update!); on this case it factors to the next utility of JavaScript sort extensions, whose single-file JavaScript module might be seen right here:

https://github.com/Tythos/jtx/blob/most important/index.js

  • This module makes use of the identical “UMD-light” (as I am calling it for now) header.



The Drawback Youngster

And now for the wild card. There may be, actually, yet one more module export method we have not talked about: ES6-style module import/export utilization. And I will be honest–I’ve spent an unhealthy portion of my weekend attempting to determine if there’s any reasonable-uncomplicated method to lengthen cross-compatibility to cowl ES6/MJS implementations. My conclusion: it will probably’t be done–at least, not with out making main compromises. Think about:

  • They’re incompatible with the CommonJS/Node REPL–so you free the flexibility to examine/check from that atmosphere

  • They’re incompatible with a outline closure/factory–so there go all of these benefits

  • They immediately contradict most of the design rules (to not point out the implementation) of the web-oriented AMD/RequireJS commonplace, together with asynchronous loading (it is within the identify, individuals!)

  • They’ve… attention-grabbing assumptions about pathing that may be very problematic throughout environments–and since it is a language-level commonplace you possibly can’t lengthen/customise it by submitting MRs to (say) the AMD/RequireJS undertaking (one thing I’ve finished a few instances)–not to say the nightmare this causes in your IDE if path contexts get blended up!

  • The tree-shaking it’s best to be capable to reverse-engineer from partial imports (e.g., image extraction) saves you actually zero something in an online atmosphere the place your largest value is simply getting the JS from the server and thru the interpreter.

If something, your greatest wager appears (like THREE.js) to solely use them to interrupt a codebase into items (if it is too huge for a single-file method, which I attempt to keep away from anyway), then combination these items at construct time (with WebPack, Browserify, and so on.) right into a module that makes use of a CommonJS/Node, AMD/RequireJS, or UMD-style header to make sure cross-compatibility. Sorry, ES6 import/export, however you could have truly made issues worse. ;(

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments