Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions NiL.JS/BaseLibrary/ShadowRealm.cs
Original file line number Diff line number Diff line change
@@ -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<string>();
return _mod.Context.Eval(str);

}

public JSValue importValue(Arguments a)
{
var path = a[0].As<string>();
var name = a[1].As<string>();
var imp = _mod.Import(path);
var promise =
new Promise(Task.FromResult<JSValue>(name == "default" ? imp.Exports.Default : imp.Exports[name]));
return _mod.Context.GlobalContext.ProxyValue(promise);
}
}
3 changes: 3 additions & 0 deletions NiL.JS/Core/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -195,6 +196,7 @@ public Context()
public Context(Context prototype)
: this(prototype, true, Function.Empty)
{

}

public Context(Context prototype, bool strict)
Expand All @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions NiL.JS/Extensions/ContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -10,6 +13,43 @@ public static void Add(this Context context, string key, object value)
context.DefineVariable(key).Assign(context.GlobalContext.ProxyValue(value));
}


/// <summary>
/// Add implementation of ShadowRealm API (EXPERIMENTAL, work by workarounds)
/// <see cref="https://github.com/tc39/proposal-shadowrealm"/>
/// </summary>
/// <param name="context"></param>
/// <param name="allowedResolvers"><see cref="IModuleResolver"/> that used for importValue</param>
/// <remarks>At current moment not allowed to use Shadow Realm in Shadow realm due to recursion</remarks>
/// <returns>Context with ShadowRealm contructor</returns>
public static Context AddShadowRealm(this Context context, IModuleResolver[] allowedResolvers = null)
{
//Workaround
context.DefineVariable("globalThis").Assign(context.ThisBind);
var resolvers = allowedResolvers ?? System.Array.Empty<IModuleResolver>();
var del = new Func<ShadowRealm>(() => new ShadowRealm(resolvers));
var func = context.GlobalContext.ProxyValue(del).As<Function>();
func.RequireNewKeywordLevel = RequireNewKeywordLevel.WithNewOnly;
context.DefineVariable("ShadowRealm").Assign(func);
return context;
}

/// <summary>
/// Add implementation of ShadowRealm API to module context
/// (EXPERIMENTAL, work by workarounds)
/// <see cref="https://github.com/tc39/proposal-shadowrealm"/>
/// </summary>
/// <param name="context"></param>
/// <param name="allowedResolvers"><see cref="IModuleResolver"/> that used for importValue. If null - used from ModuleResolversChain</param>
/// <remarks>At current moment not allowed to use Shadow Realm in Shadow realm due to recursion</remarks>
/// <returns>Context with ShadowRealm contructor</returns>
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);
Expand Down
5 changes: 4 additions & 1 deletion NiL.JS/Extensions/JSValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,9 +107,11 @@ public static T As<T>(this JSValue self)
case TypeCode.Double:
return GetDefinedOr<T>(self, (T)(object)double.NaN);
}

return GetDefinedOr<T>(self, default(T));
}



public static T GetDefinedOr<T>(this JSValue self, T defaultValue)
{
Expand Down
4 changes: 2 additions & 2 deletions NiL.JS/NiL.JS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
</PropertyGroup>

<Target Name="ReleaseNotesReading" BeforeTargets="GenerateNuspec">
<ReadLinesFromFile File="../Release.md" >
<Output TaskParameter="Lines" ItemName="ReleaseNoteLines"/>
<ReadLinesFromFile File="../Release.md">
<Output TaskParameter="Lines" ItemName="ReleaseNoteLines" />
</ReadLinesFromFile>
</Target>

Expand Down
131 changes: 131 additions & 0 deletions Tests/ShadowRealmTest.cs
Original file line number Diff line number Diff line change
@@ -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<bool>());
Assert.IsTrue(ctx.Eval("globalThis.lol").As<int>() == 123);
Assert.IsTrue(ctx.Eval("this.lol").As<int>() == 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<int>() == 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<Function>() == 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<bool>() == 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<Function>().Call(new Arguments()).As<Promise>().Task.Result.As<int>() == 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<Function>().Call(new Arguments()).As<Promise>().Task.Result.As<int>() ==
123);
}
}