Adding scriptElement.exports for configuration use cases
Nobody has claimed this yet.
- Dominant language
- HTML
- Stars
- 9.4k
- Forks
- 3.2k
- PR merge metrics
- PR metrics pending
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 https://github.com/whatwg/html/issues/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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the proposed HTMLScriptElement.exports API and the module examples using script.mjs, inline modules, and import maps. Read the prior discussion in issue #2235 and the open questions about promises, non-module scripts, JSON, and CSS modules. Done means reaching a settled specification for exports and its interaction with script and module processing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- html, javascript
- Domain
- web-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100