A few module ideas

James Browning thejamesernator at gmail.com
Sun Aug 20 05:32:11 UTC 2017


These are just some ideas I've had for improving es modules based on
my experiences with them. The syntax and stuff with them isn't too
important, the main point is the problem I'm trying to solve with each
of them, feedback and criticism is welcome.

# Dynamic modules

One of the biggest issues I've had with ES modules is not being able
to load classic scripts as part of the dependency graph, one of the
solutions I've used but am not particularly happy with is having an
async loader function e.g.:

```js
function loadScript(url) {
    return new Promise(resolve => {
        const scriptElem = document.create('script')
        scriptElem.src = url
        scriptElem.onload = resolve
    })
}

// some other file

async function computeSpline() {
    await loadScript('./mathjs.js')
    // use math here
}
```

And while this approach works somewhat it's a bit of a pain for a
couple reasons:

- If something gains a `script` dependency it necessarily breaks all
consumers by becoming asynchronous even if the original operations
were synchronous
- It's not generic for other types of resources e.g. I can't load an
image without creating another loader function or so on

---

My proposed solution, dynamic (but static) export:

```js
// math.mjs
import loadScript from ".../loadScript.js"

loadScript('./math.js').then(_ => {
    const math = window.math
    delete window.math
    export({
        math as default
    })
})
```

This solution is also generic so it can be used for loading any type
of resource:

```
// highPerformanceMath.mjs
fetch('.../math.wasm').then(response => response.arrayBuffer())
    .then(buffer => WebAssembly.instantiate(buffer, {}))
    .then(({ instance }) => {
        export({
            instance.exports as default
        })
        // or potentially named exports
        export({
            instance.exports.fastFourierTransform as fastFourierTransform
            ...
    })
```

Now this solution would be nice because it's generic and allows for
loading any (even asynchronous) object as part of the module graph and
doesn't cause explosions where because one part becomes asynchronous
everything becomes asynchronous.

However there is a deficiency in that it can be quite verbose for
similar tasks e.g. loading WebAssembly modules which is why I thought
of idea 2:

# Module Arguments

Effectively module arguments would allow passing data to a module
(statically) during loading e.g.:

```js
// some-file.js
import dict from "./dictionary.js" where { lang = "en-US" }

// dictionary.js
fetch(`./dictionaries/${ import.arguments.lang }.txt`)
    .then(response => response.text())
    .then(text => export({
        JSON.parse(text) as default
    })
```

This solves the previous problem of very similar dynamic modules for
similar types by allowing details like that to be passed in as
arguments e.g.:


```js
import math from "./loadScript.mjs" where {
    script = './math.js',
    globalName = 'math'
}
```

# Lazy Export-From

One of the nice things about named exports is you can minimize the
amount of mostly similar `import` declarations e.g.:

```js
import map from "./lodash/map.js"
import filter from "./lodash/filter.js"
import flatMap from "./lodash/flatMap.js"
...

// can become
import { map, filter, flatMap, ... } from "./lodash/lodash.js"
```

However it has a major downside of massively increasing the amount of
fetch/parse/execute time for all those additional things exported by
the combined module.

My idea is to allow modules to declare that parts need to not be
fetched parsed or executed if they're not actually imported e.g.:

```js
// my-operators-library.js
static export { map } from "./map.js"
static export { filter } from "./filter.js"
static export { reduce } from "./reduce.js"
```

Effectively all my idea adds is the `static export` (syntax not
important) form that effectively says these names should only be
resolved if they're actually imported and can be safely ignored if
they're not used. This way you get both the benefits of collection
modules (easier to `import` and reduces duplication) and the benefits
of individual `import`s (lesser loading sizes).

# Summary

Basically the ideas suggested here are to solve these particular
problems I've had with ES modules:

- Unable to load classic scripts (and other types of resources
statically e.g. conditional modules) as part of the module graph
- Unable to specify more specific behavior for a module to prevent duplication
- Either have to have lots of almost duplicate import declarations or
have to load unnecessary files

The solutions I proposed aimed to keep the constraint that module
exports should remain statically parsable which is why `export({ ...
})` shares the syntactic form.

I refrained from specifying the semantics of the specific operations
as there's details that'd need to be sorted out for all of them if
there is any interest whatsoever in implementing them.


More information about the es-discuss mailing list