-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Adding scriptElement.exports for configuration use cases #7367
Description
(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 withwaitForElement("#configuration")+ dynamicimport()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 anyimportstatement. 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