-
Notifications
You must be signed in to change notification settings - Fork 30k
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
repl: add top-level static import #17285
Conversation
72ae712
to
5178fc7
Compare
lib/repl.js
Outdated
@@ -222,6 +226,15 @@ function REPLServer(prompt, | |||
} | |||
} | |||
|
|||
if (!wrappedCmd && experimentalModules && code.startsWith('import')) { |
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.
What about leading whitespace? The code.startsWith('import')
will miss it.
lib/internal/repl/import.js
Outdated
specifiers = specifiers | ||
.map(([imported, local]) => `${local} = exports['${imported}'];`) | ||
.join('\n'); | ||
|
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.
👆 Can you provide an example of what the generated code will look like?
lib/internal/repl/import.js
Outdated
/* transforms | ||
"import { x, y as z } from 'z'; x(); z + 1" | ||
|
||
"(async () => { |
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.
@devsnek Awesome! Thanks for adding the transformation example.
Where is x
and z
initialized? Can you add that to the transformed source example?
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.
they aren't, like the await transform i rely on them attaching top scope
lib/repl.js
Outdated
@@ -226,7 +226,7 @@ function REPLServer(prompt, | |||
} | |||
} | |||
|
|||
if (!wrappedCmd && experimentalModules && code.startsWith('import')) { | |||
if (!wrappedCmd && experimentalModules && /^(\s+?)?import/.test(code)) { |
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 could see the sniff getting pretty robust... what about leading multiline comments, e.g. /*x*/ import...
Since processTopLevelImport
fails gracefully, you might just keep the preflight check super simple, e.g. /\bimport\b.test(code)
.
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'll just change it to code.includes('import') at this point
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.
Cool, the word boundary check just prevents matches for "important", "imports", etc.
BTW, I ❤️ all this official Acorn use in Node. |
@guybedford i'm getting some strange behavior with this that i'm not sure how to debug. i did some pretty extensive searching through the loader but i'm honestly not sure how this error is caused, can you take a look? |
@devsnek I think you'll need to try a few things to track it down. Some ideas which may or may not be valid - perhaps try removing the loader deletion code in https://github.com/nodejs/node/pull/17285/files#diff-27a256465ce1725db0b0222b7c754b94R8 to keep it around in memory to see if it could be some GC issue. Or try following the ModuleJob link logic to see that it is setting "this.module" to a valid module object and if so that it is persisting correctly. |
@guybedford this seems to have fixed it, i tried to trace the scopes on why this was messed up but i wasn't able to figure it out. This makes me worried that perhaps this is a fix on the issue but not the solution to the problem. @@ -91,16 +91,16 @@ class ModuleJob {
}
async run() {
- const module = await this.instantiate();
+ await this.instantiate();
try {
- module.evaluate();
+ this.module.evaluate();
} catch (e) {
e.stack;
this.hadError = true;
this.error = e;
throw e;
}
- return module;
+ return this.module;
}
}
yes i have seen it, hence |
Does this PR support live bindings? In other words, if the imported module later re-exports something that was imported earlier in the REPL, will the value of that variable/binding be correctly updated in the REPL? If not, that's is a pretty big departure from the semantics of ECMAScript modules. |
@benjamn it does not, I considered attaching every export as a getter/setter, but that seemed kinda over the top. Do other people have an opinion on this? |
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.
Please have a look at some of the projects that support live bindings in the Node REPL:
Given that other projects have gotten live bindings to work, I believe an implementation without that functionality will not be well-received by the Node community.
I would like to say this PR just needs a little work, and even offer my help cleaning it up, but I honestly think you might be going down the wrong path here, and you should take some time to learn more about the ECMAScript modules spec (especially as it relates to the REPL), get familiar with prior work in this space, and maybe pick up a few of the (many) tools that exist to do AST-based code transformation more safely than you're doing it here.
Full disclosure: I'm the author of Reify, and @jdalton is the author of @std/esm
, which is a fork of Reify.
lib/internal/repl/import.js
Outdated
.join(';'); | ||
|
||
// re-insert any code that followed the import: | ||
// "import { a, b } from y; a(); b + 5" |
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.
Please be aware that import
declarations are hoisted, so
a(); b + 5; import { a, b }
is perfectly legal, because the import
declaration is evaluated before the rest of the code.
lib/internal/repl/import.js
Outdated
// "import { a, b } from y; a(); b + 5" | ||
// ^^^^^^^^^^ | ||
const following = root.body.slice(1) | ||
.map(({ expression: { start, end } }, i) => { |
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.
What about nodes that are not ExpressionStatement
s (and thus do not have an .expression
property)?
lib/internal/repl/import.js
Outdated
x = exports['x'];z = exports['y']; | ||
}); | ||
x();return (z + 1) | ||
})();" |
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 async
function and await
expression seem unnecessary here. Instead, you could just extract any ImportDeclaration
nodes from the code, load those modules asynchronously first, and then evaluate the remaining code once the imported variables have been defined.
I believe the Node REPL's eval
API supports a callback, which should make this relatively easy, but don't quote me on that (I haven't looked at that code recently).
See my other comments about hoisting for an explanation of why extracting ImportDeclaration
s and loading them first would be more correct than what you're doing here.
lib/internal/repl/import.js
Outdated
.import(${root.body[0].source.raw}) | ||
.then((exports) => { | ||
${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.
This code needs to be capable of handling multiple import
declarations.
Okay, just throwing this out there: Since this cowpath has already been paved by the The If y'all are up for it, I'm totally down for working with Node folks and @devsnek on any features or tweaks needed to get the official Node REPL scenario running with |
@jdalton i think that initially integrating @std/esm sounds like a good idea, and i was pretty excited by it, but the cons outweigh the pros. As time moves on having all the features and syntax stuff of js being handled without much upkeep on our part is great, but it comes with some downsides.
And as a side note, also i've been planning to refactor the repl to be nicer on the eyes (getting rid of |
@jdalton if you wanted to implement it i would recommend not waiting for me to figure out how std/esm's api works. I'll still keep this open but if people want something else i say lets do whats best for everyone. |
a6cf253
to
2006a9e
Compare
@bmeck just to be clear, you're saying no to
in addition to
the second would never work, the first currently does work. p.s. please everyone put in |
What sort of APIs exist in Node and/or V8 that could enable a contributor like @devsnek to make progress on the Environment Record implementation? I don't think |
@benjamn if we implement |
I feel like separate inputs replacing the binding is fine. My example above that I would not want to work is a single input, same binding id imported, and same specifier used in both import declarations. |
In my discussion of @bmeck's examples, each line should be taken as a command entered by itself. However, I believe the only reason this matters is that the
Making this an error would be fine with me, since my only reason for wanting to allow replacement is so that you can change your mind later in the REPL session. @MylesBorins Is the REPL ever going to stop exposing |
@benjamn we can do this by rewriting the source in a slightly more complex way. I don't think V8 currently has a way to do this that can be sloppy mode.
can roughly be run in any way that acts like an environment record / fulfills the initialization using equivalents of spec based methods matching. The spec actually uses environment records that have objects backing them and we can use those, but I don't want to define the behavior in terms for descriptors. Given the above we can probably reuse most of the existing stuff and just be sure the REPL doesn't use the global environment record when running. Roughly:
Running twice. Where This avoids the leakage of the implementation being done with objects behind the scenes. |
@bmeck If you're talking about Environment Records that are actually backed by objects, then I think we're pretty much back to the implementation in this PR. The REPL I would much prefer changing the default behavior of the REPL so that As long as live bindings work, I guess I don't care if they're backed by getters that could have been defined with |
Yes, this is all just some minor implementation changes to avoid doing something that doesn't fit with spec mechanism observable behavior. I only want to prevent observable differences by explicitly stating the context object uses descriptors/is exposed in some way, the implementation being backed by Objects seems fine to me. I will need some time to fully review the PR in depth and look for any leaks, but wanted to be clear as the behaviors above were not fully defined when reading it on first read through. |
This discussion is really making me think TC39 should dedicate some time to thinking about module syntax in REPL environments, or anywhere commands are evaluated incrementally. Maybe a discussion of REPL deviations belongs in an Annex section of the ECMA-262 spec? Besides Node, I know of several TC39 members who work on DevTools for their sponsor companies, and I'm sure they've been frustrated by these issues before. |
crossreference for V8 issue "Provide a REPL version for parsing top-level code": https://bugs.chromium.org/p/v8/issues/detail?id=6903 |
As an author of original DevTools hack, I am not sure that this pull request is move into right direction. Top level await was mostly fun feature that does not change semantic significantly, contains trivial rewrites and can be easily workarounded by user by introducing top level V8 repl mode can address this issue as well and based on number of differences between normal JavaScript and JavaScript in repl - it probably requires some attention from guys who works on spec maybe it is time when we need something that will be supported by different browser and Node.js. |
@ak239 This PR does add a very specific solution to a problem. Given // ./foo
export let x = 'foo';
export function then () {
return null;
} An import declaration grabs the binding > import {x} from './foo'; x
'foo' while a dynamic import gets the result of resolving the module namespace ( > (await import('./foo')).x
TypeError: Cannot read property 'x' of null |
@ak239 Top-level Consider what you're saying: instead of typing > import assert from "assert" you would have Node REPL users type > let assert = (await import("assert")).default every time they want to import a module? That seems like a move in the wrong direction, not to mention it sacrifices live bindings. Negative comments are fine in a forum like this, but they should be constructive. Please read through this PR in detail before jumping to conclusions about how much of a hack it is, and especially before commenting to that effect. If you read my initial review comments, you'll see that I was pessimistic too, but I've come around to the goals of this approach (even if some details still need to be worked out). |
I have not implemented top-level I am pretty sure that we should have right balance and do not write own language by rewriting original JavaScript using acorn. At least we should first figure out the spec for this kind of rewriting to have the same behavior in different repl environments including Node.js repl and Chrome DevTools console. So my constructive point: collect a lot of different use cases when repl behavior should be different vs non-repl behavior, talk about this with language spec experts, implement native repl support in V8 and finally remove top level await rewriting from codebase. |
We can make progress on a REPL spec in parallel with this PR (and other REPL experimentation, for that matter). The beauty of the REPL is that it doesn't have a legacy problem. If Node makes a backwards-incompatible change to the way the REPL works, you don't have to go back and rewrite all your previous REPL commands. They worked in the past, and slightly different commands work now. In other words, we can easily change our minds about this stuff, so we shouldn't pretend we have to figure everything out before making any changes. The fact that this PR uses Acorn to parse |
@benjamn I have created https://github.com/bmeck/js-repl-goal to move discussion about such a goal there, we can make the requirements on a PR basis and I don't see any reason to block PRs that look future safe. @ak239 lots of people on this comment thread are in TC39 actually so there are plenty of language experts, we just haven't been talking fully in spec language acting as if we will try to upstream the behavior. Made a repository to talk about it, though unsure if we should propose it to TC39 without browser buy in. |
The developer ergonomics of having this does seem compelling and there seems to some appetite from the community for having this. Looking at the implementor concerns, maybe it would be useful to land this initially behind a flag (say, Then, we could get better feedback on usage numbers and patterns and make a more informed decision about having this in V8 or going through a standards body. |
it's already behind the esm flag, I don't really see a need to flag it in release, people who use nightly can bring up any issues they find |
Closing this PR for now. I personally don't think this is a good idea, and even if it becomes one we'd need much more work to get this feature fully consistent with user expectations (see https://github.com/bmeck/js-repl-goal). |
@TimothyGu I respectfully disagree with your personal opinion, and I believe this PR should remain open, unless you can provide some other reasons for closing it. For example, if you've discussed this with other Node/TSC members, and they have reasonable objections to it, that would count for more than one person's opinion, in my opinion. As another example that goes beyond one person's opinion, if you knew about a plan to remove I genuinely welcome any additional explanation you can provide. In the meantime, here are my thoughts on why this PR is valuable: Supporting static Your comment about "just bank[ing] on The discussion in this PR has focused primarily on the appropriate implementation (e.g., whether/how to use the REPL The basic behavior of REPL
If we can agree on that behavior, then I think we can work out the precise implementation and semantics over time. Please note: this freedom is unusual in programming language design! Usually you have to get everything right early on, or you'll be stuck with the wrong decisions later. Fortunately, because we're talking about the REPL, which is a session-based programming environment, there is less of a legacy code problem, as I explained in this comment. With all of that in mind, I believe this PR provides something really valuable: a chance for Node developers to use an experimental feature, gated behind the |
@benjamn for what its worth i'm also in agreement with tim, i just hadn't gotten around to closing this yet. i would love for these features to be part of node but this pr was a hack at best. |
To note, the semantics of the REPL goal linked above are not set in stone, it has not been presented at any stage to TC39. I do think having it behind a flag would be fine and even help to define what spec a REPL goal should be aiming for; but, getting declaration semantics correct is difficult without VM support. |
Adds support for top-level static import, to achieve this some
rearranging of lib/module's esm loader was needed, and it was moved to
internal/loader/singleton.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
repl, module