Skip to content

Adding scriptElement.exports for configuration use cases #7367

Open
@domenic

Description

@domenic

(Credit to @dvoytenko for this proposal.)

Problem statement

It's a common pattern to serve most of your JavaScript from CDNs, but use an inline script for configuration settings. E.g.

<script>
self.configuration = {
  deviceType: "tier1"
  /* ... */
};
</script>
<script src="https://cdn.example.net/script.js">

where https://cdn.example.net/script.js could end up using self.configuration.

This setup is fragile, as it relies on the web page author to ensure the inline script appears before any CDN-included scripts, and and doesn't play well with async="" which might cause scripts to execute early.

Additionally, some authors would prefer to use modules for this, so that the dependency between script.js and the global configuration is explicit.

Proposal

We could add an exports property to HTMLScriptElement. This was mentioned previously in #2235, but without use cases. Then we could have this setup:

<!-- Order no longer matters; async="" is fine -->
<script type="module" async src="https://cdn.example.net/script.mjs">

<script type="module" async id="configuration">
export const deviceType = "tier1";
// ...
</script>

Where script.mjs contains

const { deviceType } = await (await waitForElement("#configuration")).exports;

and waitForElement is a developer-written helper utility that uses a MutationObserver to wait for the specified element to appear.

Note that exports itself would be a promise, because in the general case the inline module might itself use top-level await. That's not what's going on in our configuration-based example, so in our case the promise will immediately fulfill, but it seems like the right primitive at the spec level.

There could also be a level of indirection, so that script.mjs doesn't need its own waitForElement function. For example if script.mjs did

import { deviceType } from "./configuration.mjs";

where https://cdn.example.net/constants.mjs contains some default configuration values, then the page itself could use an import map to remap https://cdn.example.com/configuration.mjs to its own script that looks like the following:

const { deviceType } = await (await waitForElement("#configuration")).exports;
export deviceType;

Alternative considered

Another way of accomplishing this, which is less powerful but potentially easier to use, would be to introduce the ability to directly import an inline script. Something like the following:

import { deviceType } from "document:configuration";

The problem with this idea is that the semantics of resolving document:configuration specifiers is tricky:

  • The most natural thing would be that resolving does a synchronous lookup in the current state of the document, and fails if there is no element with the specified ID. However, then you basically go back to the current state of things, just with modules: you'll still have problems if your inline module isn't before any CDN-provided modules, or if you use async="". You could combine it with waitForElement("#configuration") + dynamic import() like so:

    await waitForElement("#configuration");
    const { deviceType } = await import("document:configuration");

    but this is not much of a win.

  • Alternately, we could try to specify a semantic where if the element with such an ID doesn't exist yet, we wait until it does before finishing module resolution. This would be nice to use, but it breaks some existing properties of modules, such as how they execute in order. I.e., we would have to delay the resolution and fetching of script.mjs's dependencies until an element with the appropriate ID appears in the tree, and then we would have to go skip the usual ordering to execute that element's inline script (and any dependencies) immediately. Or, we could end up waiting indefinitely, if no such element appears. Also, these strange semantics an be caused deep in the tree, by any import statement. So this seems bad.

Combined with the idea that there might be speculative future HTML modules-related use cases for an exports property, per #2235, probably this alternative is not a good direction and we should do exports instead.

Details

We've said exports should be a promise. What about in the non-module script case? It could be a forever-pending promise, or a promise already resolved with null. Or maybe it could be null, instead of a promise? Web IDL might make the latter impossible currently...

This feature makes inline JSON and CSS modules useful. Should we consider allowing them at the same time?

/cc @whatwg/modules

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions