ES6 Loader proposed changes

# Ian Hickson (5 years ago)

Here are the changes that would be needed to make the ES6 loader infrastructure underpin the Web platform's loader infrastructure (basically, placing the ES6 loader the management layer between the Web platform APIs on top, and Service Workers underneath).

The ES6 module loader mechanism is very explicitly and specifically specified. It provides hooks that the UA can plug into, e.g. to actually dispatch loads, and it tracks the load state of each resource it is informed about, including what other resources that resource depends upon. If we can reuse this for the rest of the Web platform, it gives authors a really uniform API for all loading operations.

  • Some document types are incrementally parsed, meaning that their dependencies are discovered before the "fetch" hook has finished. For these, it would be helpful if the dependencies could be reported to the ES6 machinery before the "fetch" hook finishes. (Right now, [[Dependencies]] is initialised as a response to the "instantiate" hook, at the end of the module load.)

  • Ideally, it would be possible to provide the dependencies even earlier, e.g. at LoadModule() time, before the "normalize" hook, for the case where a custom module loader has a JSON file describing the dependency tree, or if, in future, HTML grows dependency information (the latter is likely).

  • It is possible to mutate the DOM, including the URL of resources that a document depends on. For example, you can have an <img src="foo.png"> element, and dynamically change that attribute to have the value "bar.jpeg" instead. To manage this, the [[Dependencies]] list needs to be mutable.

  • Some loads have metadata other than dependencies, too. For example, anything loaded in CSS via the @import rule will always be parsed as CSS, even if it has already been referenced as an HTML file or ES6 module or PNG image elsewhere before: the load has the metadata "treat as CSS and don't dedupe". Similarly, <link rel=stylesheet> rules can have media queries attached to them. It would therefore be helpful if, rather than just a name, you could pass LoadModule() some metadata that would be passed along to all the hooks.

With the above changes, I think we could make ES6 modules underpin the entire Web dependency management system without needing to invent and expose yet another API, and without needing to redundantly manage the dependency tree. If we reuse the ES6 module loader to manage all HTML document resources, it would also trivially allow custom module loaders to allow ES6 modules to refer to other resource types without those module loaders needing to redundantly reimplement parts of the browser infrastructure (e.g. they could just hook into existing CSS style sheet loads rather than having to do separately load style sheets mentioned in ES6 modules).

# David Herman (5 years ago)

On Aug 28, 2014, at 10:10 AM, Ian Hickson <ian at hixie.ch> wrote:

Here are the changes that would be needed to make the ES6 loader infrastructure underpin the Web platform's loader infrastructure (basically, placing the ES6 loader the management layer between the Web platform APIs on top, and Service Workers underneath). ... If we can reuse this for the rest of the Web platform, it gives authors a really uniform API for all loading operations.

The module loader isn't the right layer for the kinds of things you're trying to do. I understand you're trying to avoid having similar mechanisms at multiple layers of the platform, but repurposing the module loader system as a general purpose web resource loading system means conflating web resources and modules, which leads to a mixing of concerns exposed to the development model. Let me go into some more detail.

What the System module loader is for

First, let's talk about what the module loader is for, and what the important requirements the browser's default System loader should address. In particular, I'm not saying that there should be no reflection of web assets to the JS module system.

JS module name resolution

  • Basic name resolution for JS modules. Refer to global and relative modules.

  • Module naming conventions that work across JS ecosystems. Conventions that work on both client and server, particularly with npm.

Straw-examples:

import _ from "underscore"; // global module
import spinner from "./spinner.js"; // relative module

HTML interop

  • Making JS modules available to HTML. Allow HTML files to import from JS.

Straw-examples:

<!-- for now: -->
<script type="module">

import $ from "jquery";
import frobify from "/scripts/frobify.js";
$(".frob").each((i, elt) => {
  frobify(elt);
});
</script>

<!-- eventually, but we can save this discussion for another day: -->
<module>

import $ from "jquery";
// etc.
</module>
  • Reflecting web assets as JS modules. Make web assets available to JS as modules that export a DOM element.

Straw-example:

import icon from "./icon.png";
console.log(icon instanceof HTMLImageElement); // true

Installing HTML/CSS components via module import

  • Applying CSS via import. Mechanism for applying stylesheets by importing them from JS.

Straw-example:

import "./main.css"; // guarantees CSS is loaded and applied by the time this module executes
import $ from "jquery";
$("#main").show();
  • Installing HTML imports via import. Mechanism for installing HTML imports by importing them from JS.

Straw-example:

import widget from "./widget.html";
console.log(widget instanceof DocumentFragment); // true

Enriched response API

  • Cross-origin fetch. Cross-origin requests should succeed by returning and opaque response, just as ServiceWorker does, that cannot be inspected but can be returned back into the pipeline for execution.

  • Streaming fetch. It should be possible for a request to return a stream instead of a string, to allow asynchronous stream processing; this should use the API that results from the stream standardization process.

What the System module loader is NOT for

  • Bundling requests.

While it's attractive to use the loader for serving multiple assets in a single payload, this wants a more general solution for bundling arbitrary assets -- perhaps for performance (since despite the impending coolness of HTTP2, people will likely still use bundling as a successful optimization technique for a long time), but also for ergonomic deployment and inclusion. Jeni Tennison has done an excellent proposal based on the link tag, and I've started exploring another approach based on URLs. Meanwhile people can experiment in userland with ServiceWorker-based techniques for bundling payloads.

  • Repurposing the module registry to reflect caching of arbitrary browser fetches.

The registry is meant to record installation of things that are conceptually modules, not arbitrary network fetches of web assets. It's not the right place to store a cache of fetches. In particular, notice how above I said that it makes sense for modules to be able to import from, say, PNG files. In that case, the developer has explicitly requested that a web resource be reflected as a module, and it should be installed as a module. But using the registry for all web fetches means that non-JS is polluting the module registry with random modules reflecting arbitrary assets in the application.

  • Forcing resource management to be reflected as modules.

The ServiceWorker API gives programmatic control over fetching of network payloads and a caching API. The module loader API gives programmatic control over fetching of modules and a caching API. What you're describing is using the latter for programmatic control over fetching and caching web assets, but that means you force them -- in the exposed API -- to be reflected as modules.

  • Exposing loaded assets in a permanent global table.

The registry is a strongly mapped global table. If the DOM eliminates a resource that was loaded from the network, and that is necessarily installed in the module registry, it's pinned by the registry and uncollectable.

What I suggest

It's still not completely clear to me what your use cases are, so I'm not sure exactly how much user-visible API you need. But if you are trying to reflect the browser's fetching policies and priorities based on different types of assets, then you are looking for a layer in between ServiceWorker and module loader -- call it an asset manager or resource manager. I'd be happy to discuss this with you and see if we can flesh out the use cases and requirements to get a better handle on the problem space.

# Ian Hickson (5 years ago)

On Fri, 29 Aug 2014, David Herman wrote:

It's still not completely clear to me what your use cases are, so I'm not sure exactly how much user-visible API you need.

My goal is to avoid browsers having to implement two dependency systems (one for ES modules, one for HTML imports).

But if you are trying to reflect the browser's fetching policies and priorities based on different types of assets, then you are looking for a layer in between ServiceWorker and module loader -- call it an asset manager or resource manager.

Right. But that kind of system will have dependencies to track, including dependencies between ES6 modules, and on ES6 modules from other things, and from ES6 modules to other things. I don't understand how to make that work if we're not able to have a single dependency system.

I'd be happy to discuss this with you and see if we can flesh out the use cases and requirements to get a better handle on the problem space.

I would love that. Where and when?

You may also find the following helpful in explaining things I'm looking at at the moment related to this:

lists.w3.org/Archives/Public/public-whatwg-archive/2014Aug/0177.html

# Rob Sayre (5 years ago)

On Fri, Aug 29, 2014 at 2:23 PM, Ian Hickson <ian at hixie.ch> wrote:

On Fri, 29 Aug 2014, David Herman wrote:

It's still not completely clear to me what your use cases are, so I'm not sure exactly how much user-visible API you need.

My goal is to avoid browsers having to implement two dependency systems (one for ES modules, one for HTML imports).

Is that a goal? Browsers have a lot of dependency systems, not just two.

# Ian Hickson (5 years ago)

On Wed, 10 Sep 2014, Rob Sayre wrote:

On Fri, Aug 29, 2014 at 2:23 PM, Ian Hickson <ian at hixie.ch> wrote:

On Fri, 29 Aug 2014, David Herman wrote:

It's still not completely clear to me what your use cases are, so I'm not sure exactly how much user-visible API you need.

My goal is to avoid browsers having to implement two dependency systems (one for ES modules, one for HTML imports).

Is that a goal? Browsers have a lot of dependency systems, not just two.

My hope is that we can "explain" all the dependency systems (HTML documents that have subresources like scripts and style sheets and so on, @imports and images and so on in CSS, HTML imports, ES6 modules, etc) using the same underlying infrastructure. This would enable authors to extend browser behaviours using a common set of primitives.

I must admit though that while I initially assumed that this would be an obvious goal that browser vendors would all be eager to reach, I have yet to see anyone indicate that they're interested in this. So maybe it is in fact not a goal. I don't know.

# Brendan Eich (5 years ago)

Ian Hickson wrote:

I must admit though that while I initially assumed that this would be an obvious goal that browser vendors would all be eager to reach, I have yet to see anyone indicate that they're interested in this. So maybe it is in fact not a goal. I don't know.

I think it must be a non-goal for competing browser vendors. For a new browser, esp. bootstrapped in JS, it's a no-brainer.

This suggest going bottom-up, not top-down: try to unify two, then three, subsystems. Don't multiply risk of independent events into a tiny odds ratio.

# Ian Hickson (5 years ago)

On Thu, 11 Sep 2014, Brendan Eich wrote:

Ian Hickson wrote:

I must admit though that while I initially assumed that this would be an obvious goal that browser vendors would all be eager to reach, I have yet to see anyone indicate that they're interested in this. So maybe it is in fact not a goal. I don't know.

I think it must be a non-goal for competing browser vendors. For a new browser, esp. bootstrapped in JS, it's a no-brainer.

This suggest going bottom-up, not top-down: try to unify two, then three, subsystems. Don't multiply risk of independent events into a tiny odds ratio.

I don't understand how to do this in a backwards-compatible way. If we slowly expose more and more resource loads to the ES module loader, then pages that rely on what resources are passed to the ES module loader will fail each time we add more.

(As a trivial example, consider a page whose fetch hook does nothing, but where the page actually doesn't load any scripts so it doesn't matter. If we then add style sheets in a later verison, suddenly no sheets load and the page is unstyled, breaking the page.)

# Brendan Eich (5 years ago)

Ian Hickson wrote:

On Thu, 11 Sep 2014, Brendan Eich wrote:

Ian Hickson wrote:

I must admit though that while I initially assumed that this would be an obvious goal that browser vendors would all be eager to reach, I have yet to see anyone indicate that they're interested in this. So maybe it is in fact not a goal. I don't know.

I think it must be a non-goal for competing browser vendors. For a new browser, esp. bootstrapped in JS, it's a no-brainer.

This suggest going bottom-up, not top-down: try to unify two, then three, subsystems. Don't multiply risk of independent events into a tiny odds ratio.

I don't understand how to do this in a backwards-compatible way.

It may be that you have to keep compatibility, which means larger API surface over time, new more-unified and old less-unified subsystems coexisting. That's how the Web has grown in other areas. Original DOM didn't reflect all elements; in the '90s things evolved in layers.

Nevertheless if you can't get implementors on side, that's a strong signal.

# Ian Hickson (5 years ago)

On Thu, 11 Sep 2014, Brendan Eich wrote:

It may be that you have to keep compatibility, which means larger API surface over time, new more-unified and old less-unified subsystems coexisting. That's how the Web has grown in other areas. Original DOM didn't reflect all elements; in the '90s things evolved in layers.

I don't really understand what this would look like for the ES6 loader. Would we have different hooks for each kind of module? Or...?

Nevertheless if you can't get implementors on side, that's a strong signal.

Indeed. I don't really mind either way, for the record; I really assumed (in part from talking with Chrome people and others in #whatwg) that this was an obvious win and would be uncontroversial, especially with the recent focus on explaining the platform via ES-exposed primitives.

Obviously for me it'd be a lot simpler if we didn't integrate things like this; it would let me spec a dependency system people are asking for for subresources of HTML documents without having to worry about integrating with CSS and ES6. :-)

# Brendan Eich (5 years ago)

Ian Hickson wrote:

On Thu, 11 Sep 2014, Brendan Eich wrote:

It may be that you have to keep compatibility, which means larger API surface over time, new more-unified and old less-unified subsystems coexisting. That's how the Web has grown in other areas. Original DOM didn't reflect all elements; in the '90s things evolved in layers.

I don't really understand what this would look like for the ES6 loader. Would we have different hooks for each kind of module? Or...?

"Hook evolution" is fair -- first you'd do one class of loadables, then induct to a second and if you couldn't extend APIs compatibly, you'd have to do some forking. Perhaps not much. The path dependence cuts both ways: you may make the system strictly more complex in hindsight; you may OTOH bring along implementors and learn things via interacting with them and with developers over multiple releases that you couldn't learn trying to do it all up front in one big jump.

I'm an optimist, so I vote for smaller jumps, but whatever you can do, get implementors on board. Doesn't seem doable otherwise (or it might end up a near miss or oversold scenario-solution like appcache -- no offense, trying to learn from history in case it rhymes ;-). And you might well nail the ultimate system from a dead start without going through the smaller jumps, don't get me wrong. The odds are worse, and we won't know till implementors catch up, which could be a while.

The EWM "explaining the platform via ES-exposed primitives" doesn't work well in big jumps, since much of the system has been explained only by non-JS and leaky-abstraction (and legacycaller, etc. anti-JS abstraction) C++ implemenation code. Ideally, EWM adherents find the "narrow waist" of the existing system to cut across with low-level JS APIs that could be used to implement everything above with good performance. This is hard.