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
Support ECMAScript modules (export/import statements, modules definition) #1054
Conversation
996377d
to
c4aae01
Compare
Was wondering why Christian's name was so familiar. It's because he's been active on Jint since 2015, way before Marko. |
084a055
to
38de185
Compare
@sebastienros yeah it's been a while, I was glad to see that Jint was still alive and well! So, I went much further than I initially planned to, but I felt that without all those pieces interacting together, I wouldn't be able to trust that my approach made sense (and indeed, by adding pieces I realized a few times I approached it wrong!) There are a lot of things in this PR, so while I think a good review would be important, I'll try and raise some specific uncertainties I had myself so we can get them out of the way. I'll use the GitHub code comments to do so, I'll write a message when I reviewed it all myself at least once. After those are resolved, we can proceed with an actual review. I don't think I completely mapped to the ECMAScript specifications, but to be honest I have trouble fully understanding it. I'll try and review it the best I can and raise questions if I'm not sure. There are also a few things that are Jint-specific:
I hope this PR will see the light of day and that I didn't do anything dumb :) |
The tests fails because |
cc3cf31
to
0e0fb5a
Compare
I'm done with my first review; I added comments and questions on things that I'm not sure about. It's a pretty big PR for someone with limited knowledge of ASTs, language specifications etc but I feel like the vast majority of the code required was already written. I'm looking forward to hearing your thoughts 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.
Looking good! As you can see most of my comments were stylistic nit-picking. The import/export API via c# engine still doesn't seem intuitive to me, but I'm just one person. You really went the extra mile with this.
I don't have good guidance whether this is spec-wise "perfect", when I read the relevant parts it seemed that there were some places where "magic just happens", so I'm happy we have an interpretation of behavior that passes tests!
Jint.Tests/Runtime/ModuleTests.cs
Outdated
@@ -0,0 +1,42 @@ | |||
using Xunit; | |||
|
|||
namespace Jint.Tests.Runtime |
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 use file-scoped namespaces here, we are moving towards them
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.
Maybe this also belongs to Module folder that the next test file has?
Jint.Tests/Runtime/ModuleTests.cs
Outdated
[Fact] | ||
public void CanExportNamed() | ||
{ | ||
_engine.DefineModule(@"export const value = 'exported value';", "my-module"); |
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.
DefineModule
as a name is a bit different from the Execute
we have for scripts, this is the first Define*
as we know have Set
. The method pretty much says what it's going to do though.
Here I'm just thinkin out loud how we should formulate final engine API for 3.x...
Jint.Tests/Runtime/ModuleTests.cs
Outdated
|
||
namespace Jint.Tests.Runtime | ||
{ | ||
public class ModuleTests |
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.
And why we need to manually test these and Test262 suite isn't helping. I did investigate a bit and seems that some module related tests light up only when async/await is implemented. So it's great that you created such extensive test cases!
Jint.Tests/Runtime/ModuleTests.cs
Outdated
[Fact] | ||
public void ShouldDefineModuleFromJsValue() | ||
{ | ||
_engine.DefineModule("value", JsString.Create("hello world"), "my-module"); |
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 API doesn't seem intuitive to me. Single value cannot be a module so here's something else going on here...
// export maybe part of the name?
engine.AddModuleExport("value", JsString.Create("hello world"), "my-module")
engine.Modules.AddExport("value", JsString.Create("hello world"), "my-module")
// maybe goes too far..
engine.Advanced.Modules.AddExport("value", JsString.Create("hello world"), "my-module")
Just throwing ideas here. The overload taking just object
might bee too much also. Without it one can write:
_engine.DefineModule("value", "hello world", "my-module");
_engine.DefineModule("value", 123, "my-module");
If such functionality is needed I think it would be better to force API user to do the call JsValue.FromObject()
?
These are advanced use cases I believe allowing cool stuff but not entirely necessary to be able to run stock ES6 code.
Jint.Tests/Runtime/ModuleTests.cs
Outdated
[Fact] | ||
public void ShouldDefineModuleFromClrType() | ||
{ | ||
_engine.DefineModule<ImportedClass>("imported-module"); |
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 doesn't seem intuitive either. Module can export a class for sure, but it's not a module.
I see that the code ends into export path and this is somehow an imported module 🤷🏻♂️ So is the API for registering Import or an Export...
@@ -2,6 +2,7 @@ | |||
using Esprima.Ast; |
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.
#nullable enable?
@@ -0,0 +1,11 @@ | |||
using System; |
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.
#nullable enable?
@@ -0,0 +1,28 @@ | |||
using System; |
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.
#nullable enable?
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.
Some of my comments my collide with @lahma 's around naming.
Jint.Tests/Runtime/ModuleTests.cs
Outdated
public void ShouldExportNamed() | ||
{ | ||
_engine.DefineModule(@"export const value = 'exported value';", "my-module"); | ||
var ns = _engine.ImportModule("my-module"); |
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.
Does ImportModule
actually import a module in the environment, or does it just return it to the caller, and doing an Execute('return value')
would not work.
@@ -138,7 +138,7 @@ ObjectInstance IConstructor.Construct(JsValue[] arguments, JsValue newTarget) | |||
{ | |||
try | |||
{ | |||
var result = OrdinaryCallEvaluateBody(_engine._activeEvaluationContext, arguments, calleeContext); | |||
var result = OrdinaryCallEvaluateBody(_engine._activeEvaluationContext ?? new EvaluationContext(_engine), arguments, calleeContext); |
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 agree, this needs to be found. Please share the stacktrace when this happens.
Jint.Tests/Runtime/ModuleTests.cs
Outdated
engine.DefineModule("import { User } from './modules/user.js'; export const user = new User('John', 'Doe');", "my-module"); | ||
var ns = engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("John Doe", ns["user"].AsObject()["name"].AsString()); |
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.
Isn't it sufficient to let users access the module properties from the engine since the module was loaded? engine.GetValue("user")
just to reduce the amounts of ways to do something, which could lead to inconsistencies, or discrepancies in behavior. So ImportModule
would return nothing. (or Engine)
I will now add the var module = new ModuleBuilder();
module.ExportClass<T>("name");
module.ExportValue("name", JsValue);
engine.LoadModule(module, "module-name"); If you disagree it's now or never :D In the future, that should also allow us to do things like parsing JavaScript expressions, create multiple bindings, etc.:
|
All right I think I have something I'm happy with that solves some of the comments. Here's how you can define modules now: Method 1, using source code. Nothing new here, except the rename: engine.AddModule("module-name", "export const x = 1;"); Method 2, using the builder. This allows you to build a module with code, objects, types, etc. engine.AddModule("module-name", builder => builder
.AddSource("export const value1 = 'hello';")
.ExportType<MyType>()
.ExportValue("value2", "world")
.ExportObject("value3", new MyType())
.ExportFunction("fn", args => Console.WriteLine(args.Length))
); This is, I think, fairly discoverable (there's only AddModule there, and only two overloads for it), it allows building modules from multiple pieces of data, it can be extended later, and doesn't need to interact with engine twice. Also, bonus, it really lazy loads modules now, so even custom-built modules won't be linked until they are loaded, making it much better if you wanted to offer a large set of modules to optionally import. I feel like actually supporting node_modules wouldn't be a stretch, but I'm already way out of my initial need so shush me :D Once a module has been added, you can use In other words, I see two ways to use it:
Since |
2e115d9
to
9580f04
Compare
Two quick questions.
Right now to "execute" a module you need to "import" it in the C# context: engine.AddModule("module-name", "export const x = 1;");
var ns = engine.ImportModule("module-name");
Console.WriteLine(ns.Get("x").AsString()); This is the equivalent of:
This is why it's confusing, we're actually creating AND running modules from the same context. However to create and run modules will probably be a pretty common pattern. So to simplify it, I had two new "sugar" APIs in mind.
This keeps the same proposed semantics. var ns = engine.AddModule("module-name", "export const x = 1;").ImportModule();
Console.WriteLine(ns.Get("x").AsString());
This adds a new word, "Execute" which here means "Add And Import". It also hides the module name, making it more in line with what you expect from Execute. However it does not add any of the lexical context into the "normal" Execute" method. The module name would be automatically generated (e.g. a GUID or something like "dynamic") var ns = engine.ExecuteModule("export const x = 1;");
Console.WriteLine(ns.Get("x").AsString()); Let me know what you think :)
|
928d552
to
6309748
Compare
I may have greatly reduced availability soon; while I won't push for a merge (I'm completely aware of what maintaining an open-source project means) I'd like to know if you believe this will eventually get merged so that I can start building on top of it. If you're not sure yet, fair enough :) If it's just a matter of taking the time to fully review but you feel that the approach is sound, I'll temporarily work on this branch for my downstream projects. Thanks :) |
} | ||
|
||
protected virtual string LoadModuleSourceCode(Uri location) | ||
protected virtual string ReadAllText(Uri uri) |
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 is not reusable anymore in a sub-class. The fact that the checks are done outside will prevent custom implementation from loading urls.
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.
We'll need this one fixed before merging. I will do it otherwise.
I added only one comment, but would like to see @lahma 's answered too. |
@christianrondeau can we help out here somehow? would be nice to get this finalized and merged 🚀 |
6309748
to
2351c37
Compare
@lahma as far as I know, all of your and @sebastienros comments have been solved or I had a follow-up question (see https://github.com/sebastienros/jint/pull/1054/files) but on my end, I'm comfortable with merging in its current state. I just rebased, all tests were green :) If I missed one question let me know, I'm also eager to see this merged! Also I had some proposals that were unanswered but those can always be improvements in future PRs, since I believe it'll either be naming changes or sugar, but the general idea should be stable. |
I checked the PR and there are some comments, can you mark resolved the ones are non-actionable etc? |
@lahma I'm not allowed to mark comments as resolved :) I did go through all comments last time, most of them are still unanswered. I added a comment on as many as I could to mark the ones I think are not actionable, and deleted my own unanswered questions to reduce the scope. I don't think I can do anything more at this point (but I'm willing to!). |
I hope I'm not the broken phone here, current state is that it was mostly nit-picking and we can always fix/break things having the beta label. Pinging @sebastienros on the matter. |
Haha by default I always take for granted that I'm the one missing something until proven wrong (or right) ;) Here's what I am looking at:
But I reiterate I want to make things as simple as I can for you, if you see what I'm missing I'll do my best to fix it. |
(in case it wasn't clear, the list I posted shows everything that I can see but I have no todos on my end as far as I can tell, I'm on hold) |
@christianrondeau awesome work! thank you for pushing this through and being patient with us! |
Woot! Thanks @lahma and @sebastienros ! Since I hope to contribute again, do you have any feedback on this PR? I know it was too large (I didn't find a way to reliably test the import without export and vice versa) but otherwise, is there something I could have done to make it easier for you guys? I feel like throwing tons of questions directly in the file changes might not have been the best approach :) |
I think the process was mostly long due to the feature being both large and had no specific spec to rely on at times (implementation specific). Hard to predict all the use cases and how people would like to interact with it "outside of JS". Feedback loop is always a bit of a problem when people try to find some free time (which is taken away from something else) and are also on different time zones 🙂 |
I'll try to do a quick round with more seamless test suite integration and allowing people to run scripts with imports without jumping many hoops. I'll create a separate PR for feedback. |
@lahma I didn't mean that it wasn't fast enough, no worries!!! I just wanted to make sure my next PR is painless for you guys :) Thanks for helping me through! |
This attempts to implement ECMAScript modules, to resolve #636. This is a draft and will definitely change, but as of now, I think the basic "export named" flow is mostly correct.
What I want to implement:
import { Class1, Class2 } from 'my-module';
I do not intend on implementing module file resolution (only modules specified in C#) for this PR, but I feel like there may not be much left to do once this works.
Note that I did (and will) use code from @lahma #1051 - the main difference is that I started with the assumption that I would only implement
export
, however, to make sure my implementation is correct, I need to implement at least one simpleimport
statement.