-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: preImport #89
Conversation
await generator.install(specifier); | ||
// the new map will then have the new mappings and the old mappings | ||
importMap = generator.getMap(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this function doesn’t return anything, how are its results preserved for use in resolve
later? As part of the internal state of generator
or something it references?
If that’s the case, it might be good for the example to illustrate this. Maybe a variable could be declared at the top level of the loader that gets referenced by both hooks, and the contents of this variable are how the information determined by preImport
gets shared with resolve
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The importMap
mutable variable is the shared state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, I missed this. I don’t know if it’s just me or if it’s likely to be nonobvious to others; if the latter, maybe add a comment inside resolve
to the effect of // Reference same `importMap` variable modified within `preImport`
. That would make the expected usage clear, so people know how this hook is expected to work.
I don't have the background context on why const results = new Map();
export async function preImport(specifier, context) {
const key = makeKey(specifier, context); // some deterministic mapping to (say) a string
const result = /* some async operation here */;
results.set(key, result);
}
export function resolve(specifier, context) {
return results.get(makeKey(specifier, context));
} At which point, the details of I assume I must be missing something; for example is there some data which is only available at the (synchronous) |
}); | ||
await generator.install(specifier); | ||
// the new map will then have the new mappings and the old mappings | ||
importMap = generator.getMap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is a race condition; old importMap
is provided to potentially multiple Generator
s (as called out in the description: all run in parallel), then importMap
is overridden with the results of each as they finish, so some results will get clobbered.
I'm not so concerned about this specific example, but more that this demonstrates how easy it is to make this sort of mistake with the API as proposed (parallel execution combined with forcing the output to be via global state is likely to cause many issues like this)
|
||
The `preImport` hook is called for each top-level import operation by the module | ||
loader, both for the host-called imports (ie for the main entry) and for dynamic | ||
`import()` imports. These are distinguished by the `dynamic` context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there was some conversation about the back-and-forth of dynamic
vs topLevel
. I don't have an opinion either way, but note that this is currently inconsistent with line 19.
Because |
The `preImport` hook allows for tracking and asynchronous setup work for every | ||
top-level import operation. It has no return value, although it can return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This “top-level” qualifier is worth calling out. What’s the precise definition? Any static or dynamic import
in the application entry point file? Only dynamic imports in that entry point?
Is that whatever data gets created by preImport
then available for all resolve
operations, not just top-level specifiers?
Since this is something of an initialization-of-the-module-graph thing, not run for every import, would it make sense for this hook to just run once and be passed in an array of specifiers? Like preImport(specifiers: string[], context: object)
where specifiers
would be the list of all top-level import specifiers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The definition is any import which calls ECMA function 16.2.1.5.2 Evaluate.
It is called for all dynamic imports and the main entry point, and any module worker thread instantiation, and will be called with the exact specifier passed to those.
It's singular and not a list because the timing is unique to each specifier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ECMA function 16.2.1.5.2 Evaluate is incomprehensible to me.
Say an application consists of these two files. Which imports get preImport
called?
// entry.js
import 'a';
await import('b');
import './c.js';
// c.js
import 'd';
await import('e');
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the example, it would get called for entry.js
, then 'b'
when that import is hit, and then 'e'
when that dynamic import is hit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so perhaps the documentation should say something along the lines of this:
The
preImport
hook is called for the root of every tree of modules imported by a dynamicimport
: the initial entry point itself (the file passed tonode
on the command line) and any dynamicimport()
calls within. The way ES modules work is that an entry point is read and statically analyzed, and all the staticimport
statements are themselves analyzed and followed and their files read and analyzed, and so on until all the leaves of the tree have been parsed. Then all this code is evaluated; and during this evaluation phase the dynamicimport()
statements are followed, which uncover new trees of modules and sub-modules. The same analysis and evaluation process then begins for each of them.preImport
happens after the analysis but before the evaluation of each set of modules.
I would think that it could be called with all the specifiers within the tree of modules for which it’s being called? So in this example, the first call could be for entry.js
, a
, c.js
and the second call would be for b
, d
and the third call would be for e
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the example, it would get called for
entry.js
, then'b'
when that import is hit, and then'e'
when that dynamic import is hit.
why wouldn't it get called for 'a'
, './c.js'
, and 'd'
? Standard imports like that can still be async anyway, as I understand it? From @GeoffreyBooth 's comment, I understood that the only place where it would not be called is when a user invokes import.meta.resolve
(which I think is a problem, but if this isn't even called when doing a regular import then I'm struggling to see the point)
What's the intent for how This will lead to very confusing behaviour where I wonder:
|
I haven't had a chance to read this entire thread, so I might be repeating stuff, but here are my thoughts ahead of the meeting: Thought experiment to validate proposalsUseful thought experiment for discussion:
^-- Loaders for those use-cases need to compose with each other. (compose/chain/whatever) Concern that
|
Should we keep this open? Per our last meeting this is the backup plan if we can’t get an appears-to-be-sync |
Closing as we’re going in a different direction per the last meeting, but happy to reopen if there’s some reason to keep working on this in parallel. |
This adds a
preImport
hook proposal.The main text here corresponds to the readme for the PR, while including a more comprehensive end-to-end example.
Naming is still provisional, specifically the
topLevel
boolean context name and thepreImport
hook name are still being ironed out.