-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Module declarations/expressions right now capture bindings of other module declarations:
let a = 1;
module A {}
module B {
a; // ReferenceError
import A; // works, module declarations are captured
}
When advancing this proposal to Stage 2, different concerns have been raised due to the different scoping behavior of this new binding type.
Solution
While discussing with @lucacasonato about this problem, we came up with a possible solution:
- Module declarations bindings behave like other bindings: they are not captured by other module declarations
- We introduce a new
import.parent
meta property, that:- In file-level modules is
null
- In module declarations/expressions, is the
Module
object that syntactically encloses the current module
- In file-level modules is
- We introduce the new
import ... from import.parent;
to import from the parent module.
Drawbacks
-
Module declarations need to be exported to expose them to children modules:
module A {} export module B {} module C { import { A } from import.parent; // Doesn't work import { B } from import.parent; // ok }
-
It's not possible to directly import from ancestors other than the parent, unless they re explicitly re-exported by the parent module (for example, using the import reflection proposal).
Currently module declarations allow doing this:export module A {} export module B { module C { import A; } }
and it would need to be re-written to this:
export module A {} export module B { export { A } from import.parent; // explicitly re-export module C { import { A } from import.parent; import A; } }
However, the vast majority of use cases for module declarations is with no nesting, so this shouldn't hurt usability much in practice. Additionally, it simplifies refactoring because you only have to pay attention to
import.parent
rather than to all the module bindings higher in the scope chain.
Example
Consider this example with the current proposal:
export module A {
export let a = 1;
}
export module B {
import { a } from A;
export let b = a * 2;
}
it would be rewritten as follows:
export module A {
export let a = 1;
}
export module B {
import { A } from import.parent;
import { a } from A;
export let b = a * 2;
}
FAQ
-
Why
import.parent
?
We considered different alternatives, such as just aparent
identifier (with a restriction that prevents module declarations from being namedparent
), orsuper
(proposed in How to import from the parent module? #20). However, the meta-property-based syntax has the advantage that it can also work in dynamic imports:export let x = 1; module A { import { x } from import.parent; console.log(x); } module B { const { x } = await import(import.parent); console.log(x); }
-
How does this interact with the
Module
constructor?
Compartments Layer 0 expands theModule
constructor so that it can be used to customize the linking behavior of modules:new Module(source, { importHook(specifier) { ... } });
Integrating the module declarations proposal with such constructor was incredibly challenging, because we needed a static way of representing the module declarations captured by the outer scope. Something like the following:
new Module(source, { importHook(specifier) { ... }, capturedStaticModules: { A: new Module(...) } });
and
A
would have been magically injected as visible in the constructed module's scope.With this
import.parent
simplification, theModule
constructor could simply accept an optionalparentModule
property, whose value is then exposed asimport.meta
without affecting the visible bindings:new Module(source, { importHook(specifier) { ... }, parentModule: parent, });