diff --git a/NiL.JS/BaseLibrary/ShadowRealm.cs b/NiL.JS/BaseLibrary/ShadowRealm.cs new file mode 100644 index 000000000..9c95ee124 --- /dev/null +++ b/NiL.JS/BaseLibrary/ShadowRealm.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using NiL.JS.Core; +using NiL.JS.Core.Interop; +using NiL.JS.Extensions; + +namespace NiL.JS.BaseLibrary; + + +[RequireNewKeyword] + +public sealed class ShadowRealm +{ + private readonly Module _mod; + + internal ShadowRealm(IModuleResolver[] allowedModules) + { + GlobalContext ctx = null; + + //We spawn new thread because not possible to create new GlobalContext in current thread when we run context + var t = new Thread(() => + { + ctx = new GlobalContext(); + }); + t.Start(); + t.Join(); + _mod = new Module("", Script.Parse(""), ctx); + _mod.Context.DefineVariable("globalThis").Assign(_mod.Context.ThisBind); + foreach (var moduleResolver in allowedModules) + { + _mod.ModuleResolversChain.Add(moduleResolver); + } + } + + + + public JSValue evaluate(Arguments a) + { + var str = a[0].As(); + return _mod.Context.Eval(str); + + } + + public JSValue importValue(Arguments a) + { + var path = a[0].As(); + var name = a[1].As(); + var imp = _mod.Import(path); + var promise = + new Promise(Task.FromResult(name == "default" ? imp.Exports.Default : imp.Exports[name])); + return _mod.Context.GlobalContext.ProxyValue(promise); + } +} \ No newline at end of file diff --git a/NiL.JS/Core/Context.cs b/NiL.JS/Core/Context.cs index b4c737080..9d624aeb8 100644 --- a/NiL.JS/Core/Context.cs +++ b/NiL.JS/Core/Context.cs @@ -7,6 +7,7 @@ using System.Threading; using NiL.JS.BaseLibrary; using NiL.JS.Core.Functions; +using NiL.JS.Extensions; using NiL.JS.Statements; #if NET40 @@ -195,6 +196,7 @@ public Context() public Context(Context prototype) : this(prototype, true, Function.Empty) { + } public Context(Context prototype, bool strict) @@ -207,6 +209,7 @@ public Context(Context prototype, bool strict) public Context(bool strict) : this(CurrentGlobalContext, strict) { + } internal Context(Context prototype, bool createFields, Function owner) diff --git a/NiL.JS/Extensions/ContextExtensions.cs b/NiL.JS/Extensions/ContextExtensions.cs index 0d416357d..24aabbed0 100644 --- a/NiL.JS/Extensions/ContextExtensions.cs +++ b/NiL.JS/Extensions/ContextExtensions.cs @@ -1,5 +1,8 @@ using System; +using NiL.JS.BaseLibrary; using NiL.JS.Core; +using NiL.JS.Core.Functions; +using Array = NiL.JS.BaseLibrary.Array; namespace NiL.JS.Extensions { @@ -10,6 +13,43 @@ public static void Add(this Context context, string key, object value) context.DefineVariable(key).Assign(context.GlobalContext.ProxyValue(value)); } + + /// + /// Add implementation of ShadowRealm API (EXPERIMENTAL, work by workarounds) + /// + /// + /// + /// that used for importValue + /// At current moment not allowed to use Shadow Realm in Shadow realm due to recursion + /// Context with ShadowRealm contructor + public static Context AddShadowRealm(this Context context, IModuleResolver[] allowedResolvers = null) + { + //Workaround + context.DefineVariable("globalThis").Assign(context.ThisBind); + var resolvers = allowedResolvers ?? System.Array.Empty(); + var del = new Func(() => new ShadowRealm(resolvers)); + var func = context.GlobalContext.ProxyValue(del).As(); + func.RequireNewKeywordLevel = RequireNewKeywordLevel.WithNewOnly; + context.DefineVariable("ShadowRealm").Assign(func); + return context; + } + + /// + /// Add implementation of ShadowRealm API to module context + /// (EXPERIMENTAL, work by workarounds) + /// + /// + /// + /// that used for importValue. If null - used from ModuleResolversChain + /// At current moment not allowed to use Shadow Realm in Shadow realm due to recursion + /// Context with ShadowRealm contructor + public static Module AddShadowRealm(this Module module, IModuleResolver[] allowedResolvers = null) + { + AddShadowRealm(module.Context, allowedResolvers ?? module.ModuleResolversChain.ToArray()); + return module; + } + + public static void Add(this Context context, string key, JSValue value) { context.DefineVariable(key).Assign(value); diff --git a/NiL.JS/Extensions/JSValueExtensions.cs b/NiL.JS/Extensions/JSValueExtensions.cs index c54a19be4..de5606a3f 100644 --- a/NiL.JS/Extensions/JSValueExtensions.cs +++ b/NiL.JS/Extensions/JSValueExtensions.cs @@ -5,6 +5,7 @@ using System.Reflection.Emit; using NiL.JS.BaseLibrary; using NiL.JS.Core; +using NiL.JS.Core.Functions; using NiL.JS.Core.Interop; namespace NiL.JS.Extensions @@ -106,9 +107,11 @@ public static T As(this JSValue self) case TypeCode.Double: return GetDefinedOr(self, (T)(object)double.NaN); } - + return GetDefinedOr(self, default(T)); } + + public static T GetDefinedOr(this JSValue self, T defaultValue) { diff --git a/NiL.JS/NiL.JS.csproj b/NiL.JS/NiL.JS.csproj index 63906a965..9cac40d14 100644 --- a/NiL.JS/NiL.JS.csproj +++ b/NiL.JS/NiL.JS.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Tests/ShadowRealmTest.cs b/Tests/ShadowRealmTest.cs new file mode 100644 index 000000000..02de8e0b3 --- /dev/null +++ b/Tests/ShadowRealmTest.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NiL.JS; +using NiL.JS.BaseLibrary; +using NiL.JS.Core; +using NiL.JS.Extensions; + +namespace Tests; + +[TestClass] +public class ShadowRealmTest +{ + private sealed class DelegateModuleResolver : IModuleResolver + { + private readonly ModuleResolverDelegate _moduleResolverDelegate; + + public delegate bool ModuleResolverDelegate(ModuleRequest moduleRequest, out Module result); + + public DelegateModuleResolver(ModuleResolverDelegate moduleResolverDelegate) + { + _moduleResolverDelegate = moduleResolverDelegate ?? + throw new ArgumentNullException(nameof(moduleResolverDelegate)); + } + + public bool TryGetModule(ModuleRequest moduleRequest, out Module result) + { + return _moduleResolverDelegate(moduleRequest, out result); + } + } + + + [TestMethod] + public void GlobalThisExist() + { + var ctx = new Context().AddShadowRealm(); + ctx.DefineVariable("lol").Assign(123); + Assert.IsTrue(ctx.Eval("globalThis === this").As()); + Assert.IsTrue(ctx.Eval("globalThis.lol").As() == 123); + Assert.IsTrue(ctx.Eval("this.lol").As() == 123); + } + + [TestMethod] + public void ShadowRealmEvaluateExpressionAndReturnNumber() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +var result = realm.evaluate(`(function () { +globalThis.lol = 123 +return globalThis.lol })()`)"); + + Assert.IsTrue(ctx.GetVariable("result").As() == 123); + } + [TestMethod] + public void ShadowRealmChangingPrimitiveInRealmDontAffectOnGlobal() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +realm.evaluate(`Array.prototype.patch = function() {}`) +var result = Array.prototype.patch"); + + Assert.IsTrue(ctx.GetVariable("result").As() == null); + } + + [TestMethod] + public void ShadowRealmGlobalThisNotContextModuleThis() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +var result = globalThis === realm.evaluate(`globalThis`)"); + + Assert.IsTrue(ctx.GetVariable("result").As() == false); + } + + [TestMethod] + public void ShadowRealmImportValueMustCallModuleResolver() + { + var ctx = new Context().AddShadowRealm(new[] + { + new DelegateModuleResolver(((ModuleRequest request, out Module result) => + { + result = null; + if (request.AbsolutePath == "/test.js") + { + result = new Module("export default testing = 123"); + return true; + } + + return false; + })) + }); + ctx.Eval(@"async function test() { +const realm = new ShadowRealm() +var result = await realm.importValue('./test.js', 'default') +return result +}"); + + + Assert.IsTrue( + ctx.GetVariable("test").As().Call(new Arguments()).As().Task.Result.As() == 123); + } + + [TestMethod] + public void ShadowRealmInModuleShouldWork() + { + var ctx = new Module(@" +const realm = new ShadowRealm() +export default async function() { +return await realm.importValue('./test.js', 'default') +} ").AddShadowRealm(new[] + { + new DelegateModuleResolver(((ModuleRequest request, out Module result) => + { + result = null; + if (request.AbsolutePath == "/test.js") + { + result = new Module("export default testing = 123"); + return true; + } + + return false; + })) + }); + ctx.Run(); + + Assert.IsTrue(ctx.Exports.Default.As().Call(new Arguments()).As().Task.Result.As() == + 123); + } +} \ No newline at end of file