ESM Loader Hooks (20.8) should support a more testable, less stateful design
What is the problem this feature will solve?
The current ESM Loader Hooks are defined as modules having the following named exports: initialize()
, resolve()
and load()
. The initialize()
function is passed data specified when calling register()
. The intent is that the other two hooks can thus also leverage this data in extensible, future-proof ways.
The problem that this introduces is that the ESM Load Hook module is now a stateful singleton because it encourages designs wherein initialization state is shared at the module level. This means that any tests looking to exercise the loader module needs to deal with ESM cache invalidation. It also makes it slightly less ergonomic (and type-safe!) to set and access any needed shared state.
What is the feature you are proposing to solve the problem?
I propose a slight change (or addition) to the proposed design. The change I'd like to propose is that the initialize()
function should be able to produce the object implementing the other two components of the interface: resolve()
and load()
.
In this way:
- Tests looking to test the loader's implementation can easily produce multiple instances of the loader for testing different inputs and scenarios in isolation.
- State can be passed in a simple, immutable and type-safe manner between the
initialize()
call and theresolve()
andload()
hooks. - A Loader could then be implemented as a class that is instantiated and returned by the
initialize()
function. The class can hold shared state in a private field or instance properties. Alternatively, theinitialize()
closure could also be used to hold this state in a way that the two other hooks can easily access.
export async function initialize(data) {
return new MyLoader(data);
}
class MyLoader {
#port;
constructor(data) {
this.#port = data.port;
}
resolve(specifier, context, nextResolve) {
this.#port.postMessage(['resolve', specifier, context]);
// No-op, defer to built-in
return nextResolve(specifier, context);
}
load(url, context, nextLoad) {
this.#port.postMessage(['load', url, context]);
// No-op, defer to built-in
return nextLoad(url, context);
}
}
What alternatives have you considered?
No response