Hi, just spitballing here, so sorry if I meander at all!!
(Prerequisite reading: "In ES6, imports are live read-only views on exported-values. …")
(Additional reading: "The answer is to use "init functions". …")
I thought about a syntax pattern that currently doesn’t exist in JavaScript and could make for some neat use cases:
export yield 'o'.repeat(5) + '...';
export yield 123;
export yield 'some expression, with no binding at all!';
This here would be much the same as a “top-level yield”, along the lines of top-level await. From the importing module’s perspective, you’d import the “iterable export”…
import *iterableExport from './module.js';
…and then, well, iterate it:
for (const x of iterableExport) {
console.log(x);
}
// -> ooooo...
// -> 123
// -> some expression, with no binding at all!
The iterable export is accessible, existing as an iterator, as soon as the module is loaded—earlier than any code (let alone all code) from the module is actually evaluated, from the perspective of the importing module. Just like a generator, execution proceeds top to bottom and pauses at the point of each no-binding export. I’m not really up to snuff on how hoisting works in (circular) JavaScript modules, but I’d expect that although named exports exist (of course) as soon as the module is imported, they don’t get a value (and/or throw an error) until execution reaches that point.
// module.js
export const a = 'apple';
export const b = 'banana';
export yield;
export const c = 'sarcophagus';
export let d = 'dracula';
export var e = 'eucalyptus';
// index.js
import {a, b, c, d, e}, *iter from './module.js';
console.log(a); // apple
console.log(b); // banana
console.log(c); // error - temporal dead zone
console.log(d); // error - temporal dead zone
console.log(e); // undefined
iter.next(); // {value: undefined, done: false}
console.log(a); // apple
console.log(b); // banana
console.log(c); // sarcophagus
console.log(d); // dracula
console.log(e); // eucalyptus
The iterable export only exists once per module. So if two other modules import the same module, they can access the exact same object. The iterable export, importantly, is not exhausted before yielding control to the importing module. This goes universally: the iterable export is never implicitly exhausted. Some sequence of for..of, next(), yield*, export yield* etc—anything explicitly iterating the iterable!—is the only way to advance execution within the module, past one or more export yield statements.
This is all contrasted to top-level await. I admit I have never used top-level await LOL. But my understanding is that the importing module really waits until all top-level awaits (in the imported module) are complete, before executing any of its own code, or before “resolving” that module and letting any higher-up importing module continue, either. Here, execution of statements within the imported module (top to bottom) is controlled by the importing module, or whoever accesses the iterable. This could be delayed arbitrarily (ex. waiting for user input) or not even happen at all.
Just like in a generator function, execution of the imported module happens implicitly until the first yield is reached. Unlike existing uses of export which I think are all statements, export yield is an expression, and the result is just what gets passed into iterableExport.next():
export let condition = 'peachy';
export let step = 0;
while (true) {
switch (export yield ++step) {
case 'throw ball': condition = 'ecstatic'; break;
case 'offer steak': condition = 'ecstatic'; break;
case 'bedtime': condition = 'zoomies'; break;
...
}
}
import {condition}, *fido from './fido.js';
condition // peachy
fido.next('throw ball') // {result: 1}
condition // ecstatic
fido.next('offer steak') // {result: 2}
condition // ecstatic
fido.next('bedtime') // {result: 3}
condition // zoomies
I think that’s basically it for the “what’s it do?” explainer!
The original syntax I had in mind was just plain old export ‘an expression', but I didn’t really like it because it doesn’t speak to what it means or what it does, and it’s too easy to accidentally run into that syntax. The new syntax, export yield ‘an expression’, also has the nifty side effect of permitting this syntax:
export yield function apple() { ... };
export yield function apple() { ... };
export yield function banana() { ... };
export yield function apple() { ... };
…which would otherwise get clunky and unpleasant:
export (function apple() { ... });
export (function apple() { ... });
export (function banana() { ... });
export (function apple() { ... });
// This module is getting on a nasty migraine...
The motivation is mostly just that I think the order that statements in a module are actually evaluated is interesting, and I’d like to give code authors control over it in a really expressive and simple way.
The counter-motivation is that the workaround is accessibly chill too ![]()
export let condition = 'peachy';
export let step = 0;
export const iterable = (function*() {
while (true) {
switch (yield ++step) {
case '...': ...
}
}
})();
import {condition, iterable as fido} from './fido.js';
condition // peachy
fido.next('throw ball') // {result: 1}
condition // ...
Like I said, I’m spitballing, so this is a fledgling idea and I don’t have snazzy real-world use cases for it laid out yet. (The workaround being nice and easy means maybe I’ll try and run into some, though.) I do offer that I come from a codebase where all my JavaScript files are sorted into various directories/folders. Everything is normal ES modules, and all the modules in a directory have a common structure, but there are several of those structures (one per directory) throughout the codebase as a whole. I like using the “shape” of a file as a way of expressing its meaning or purpose, especially in the ways it’s similar to and different from other files sharing the same structure. I don’t care much about what is considered anti-patterns (I scoff!) or atypical uses of language features. I can get over boilerplate, but I always enjoy when what I’m doing is more directly integrated with the language… thus this suggestion for a new kind of per-file expression!