A fairly classic problem with JS module evaluation order is that if there is a circular evaluation with a top-level reference to some value in one of the modules, we can get an error that occurs depending on which module was imported first.
For those not familiar or who need a reminder, consider the following example (using TS types for clarity):
import AbsolutePath from "./AbsolutePath.js";
import RelativePath from "./RelativePath.js";
// In this case Path is an abstract base class, all instances *must* either
// be absolute or relative paths as other possibilities are impossible, if people
// actually want to subclass they can subclass those classes
export default abstract class Path {
static isPath(value: any): value is Path {
return #pathSegments in Path;
}
readonly #pathSegments: string[];
constructor(pathSegments: string[]) {
// All actual instances need to be one of the concrete subclasses
// further subclassing is allowed from there
if (!AbsolutePath.isPrototypeOf(new.target)
&& !RelativePath.isPrototypeOf(new.target)) {
throw new TypeError("Path cannot be instantiated directly, use either AbsolutePath or RelativePath instead");
}
}
// ... the common methods between path kinds like .normalize(), .parentDir(), etc
}
import Path from "./Path.js";
export default class AbsolutePath extends Path {
// ... absolute path specific stuff
}
import Path from "./Path.js";
export default class RelativePath extends Path {
// ... relative path specific stuff
}
Now, importing these modules can potentially fail with a reference error, in particular if you try importing from the Path file first then a reference error happens because the discovered evaluation order is:
AbsolutePath.js // Oops, this happens first because the depth-first search finds AbsolutePath.js as the first entry
RelativePath.js
Path.js
This can be seen in a tree diagram of the situation:
We can actually fix this by injecting an additional module that contains the implementation of Path and a new wrapper that just exports those definitions but imports the other modules as well.
In particular we move the contents of Path.js into a new file, say Path.impl.js, point those modules to this impl file, and add a new Path.js file which forces the evaluation order:
// Path.js
// Import Path first so it evaluates first
import "./Path.js";
import "./AbsolutePath.js";
import "./RelativePath.js";
// Export the contents of Path so that this module looks like
// the real path module
export * from "./Path.impl.js";
export { default as Path } from "./Path.impl.js";
Again this can be more easily seen in a diagram:
However it's a bit unfortunate we need to inject the additional module and there can be confusion about which files subclasses should refer to (it technically doesn't matter in this case, as it won't change evaluation order, but the fact this is the case is fairly subtle).
But this proposal by allowing inline modules, can solve this by allowing us to instead of having to inject a Path.impl.js module, we could instead just put inside Path.js:
import PathImpl; // Ensure PathImpl evaluates before any subclasses
import AbsolutePath from "./AbsolutePath.js";
import RelativePath from "./RelativePath.js";
module PathImpl {
export default class Path {
// ...
}
}
export * from PathImpl;
export { default as Path } from PathImpl;
And voila, we've forced an evaluation for circular modules without any file hacks (or other hacks that involve exporting init functions and the like). The subclass files don't even need to know that there is a hidden module, instead they can just refer to Path.js without knowing anything about the extra injected module.
A fairly classic problem with JS module evaluation order is that if there is a circular evaluation with a top-level reference to some value in one of the modules, we can get an error that occurs depending on which module was imported first.
For those not familiar or who need a reminder, consider the following example (using TS types for clarity):
Now, importing these modules can potentially fail with a reference error, in particular if you try importing from the
Path
file first then a reference error happens because the discovered evaluation order is:AbsolutePath.js
// Oops, this happens first because the depth-first search findsAbsolutePath.js
as the first entryRelativePath.js
Path.js
This can be seen in a tree diagram of the situation:
We can actually fix this by injecting an additional module that contains the implementation of
Path
and a new wrapper that just exports those definitions but imports the other modules as well.In particular we move the contents of
Path.js
into a new file, sayPath.impl.js
, point those modules to this impl file, and add a newPath.js
file which forces the evaluation order:Again this can be more easily seen in a diagram:
However it's a bit unfortunate we need to inject the additional module and there can be confusion about which files subclasses should refer to (it technically doesn't matter in this case, as it won't change evaluation order, but the fact this is the case is fairly subtle).
But this proposal by allowing inline modules, can solve this by allowing us to instead of having to inject a
Path.impl.js
module, we could instead just put insidePath.js
:And voila, we've forced an evaluation for circular modules without any file hacks (or other hacks that involve exporting init functions and the like). The subclass files don't even need to know that there is a hidden module, instead they can just refer to
Path.js
without knowing anything about the extra injected module.