Skip to content

Commit

Permalink
Refactored importmap scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
tomvanenckevort committed Mar 18, 2024
1 parent 9b59f35 commit 11078c2
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 94 deletions.
27 changes: 24 additions & 3 deletions src/AngleSharp.Js.Tests/EcmaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,40 @@ public async Task ModuleScriptWithScopedImportMapShouldRunCorrectScript()
{
{ "/example-module-1.js", "export function test() { document.getElementById('test1').remove(); }" },
{ "/example-module-2.js", "export function test() { document.getElementById('test2').remove(); }" },
{ "/test.js", "import { test } from 'example-module'; test();" },
{ "/test/test.js", "import { test } from 'example-module'; test();" }
}))
.WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });

var context = BrowsingContext.New(config);
var html = "<!doctype html><div id=test1>Test</div><div id=test2>Test</div><script type=importmap>{ \"imports\": { \"example-module\": \"/example-module-1.js\" }, \"scopes\": { \"/test/\": { \"example-module\": \"/example-module-2.js\" } } }</script><script type=module>import { test } from 'example-module'; test();</script>";

var document1 = await context.OpenAsync(r => r.Content(html));
var html1 = "<!doctype html><div id=test1>Test</div><div id=test2>Test</div><script type=importmap>{ \"imports\": { \"example-module\": \"/example-module-1.js\" }, \"scopes\": { \"/test/\": { \"example-module\": \"/example-module-2.js\" } } }</script><script type=module src=/test.js></script>";
var document1 = await context.OpenAsync(r => r.Content(html1));
Assert.IsNull(document1.GetElementById("test1"));
Assert.IsNotNull(document1.GetElementById("test2"));

var document2 = await context.OpenAsync(r => r.Content(html).Address("http://localhost/test/"));
var html2 = "<!doctype html><div id=test1>Test</div><div id=test2>Test</div><script type=importmap>{ \"imports\": { \"example-module\": \"/example-module-1.js\" }, \"scopes\": { \"/test/\": { \"example-module\": \"/example-module-2.js\" } } }</script><script type=module src=/test/test.js></script>";
var document2 = await context.OpenAsync(r => r.Content(html2));
Assert.IsNull(document2.GetElementById("test2"));
Assert.IsNotNull(document2.GetElementById("test1"));
}

[Test]
public async Task ModuleScriptWithAbsoluteUrlImportMapShouldRun()
{
var config =
Configuration.Default
.WithJs()
.With(new MockHttpClientRequester(new Dictionary<string, string>()
{
{ "/jquery_4_0_0_esm.js", Constants.Jquery4_0_0_ESM }
}))
.WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });

var context = BrowsingContext.New(config);
var html = "<!doctype html><div id=test>Test</div><script type=importmap>{ \"imports\": { \"https://example.com/jquery.js\": \"/jquery_4_0_0_esm.js\" } }</script><script type=module>import { $ } from 'https://example.com/jquery.js'; $('#test').remove();</script>";
var document = await context.OpenAsync(r => r.Content(html));
Assert.IsNull(document.GetElementById("test"));
}
}
}
18 changes: 9 additions & 9 deletions src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using AngleSharp.Io;
using AngleSharp.Io.Network;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AngleSharp.Js.Tests.Mocks
{
using AngleSharp.Io;
using AngleSharp.Io.Network;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Mock HttpClientRequester which returns content for a specific request from a local dictionary.
/// </summary>
Expand Down
91 changes: 25 additions & 66 deletions src/AngleSharp.Js/EngineInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace AngleSharp.Js
using Jint.Native.Object;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

sealed class EngineInstance
Expand All @@ -21,25 +19,19 @@ sealed class EngineInstance
private readonly ReferenceCache _references;
private readonly IEnumerable<Assembly> _libs;
private readonly DomNodeInstance _window;
private readonly IResourceLoader _resourceLoader;
private readonly IElement _scriptElement;
private readonly string _documentUrl;
private readonly JsImportMap _importMap;

#endregion

#region ctor

public EngineInstance(IWindow window, IDictionary<String, Object> assignments, IEnumerable<Assembly> libs)
{
_resourceLoader = window.Document.Context.GetService<IResourceLoader>();

_scriptElement = window.Document.CreateElement(TagNames.Script);

_documentUrl = window.Document.Url;
_importMap = new JsImportMap();

_engine = new Engine((options) =>
{
options.EnableModules(new JsModuleLoader(this, _documentUrl, false));
options.EnableModules(new JsModuleLoader(this, window.Document, false));
});
_prototypes = new PrototypeCache(_engine);
_references = new ReferenceCache();
Expand Down Expand Up @@ -81,6 +73,8 @@ public EngineInstance(IWindow window, IDictionary<String, Object> assignments, I

public Engine Jint => _engine;

public JsImportMap ImportMap => _importMap;

#endregion

#region Methods
Expand All @@ -89,7 +83,7 @@ public EngineInstance(IWindow window, IDictionary<String, Object> assignments, I

public ObjectInstance GetDomPrototype(Type type) => _prototypes.GetOrCreate(type, CreatePrototype);

public JsValue RunScript(String source, String type, JsValue context)
public JsValue RunScript(String source, String type, String sourceUrl, JsValue context)
{
if (string.IsNullOrEmpty(type))
{
Expand All @@ -109,7 +103,7 @@ public JsValue RunScript(String source, String type, JsValue context)
else if (type.Isi("module"))
{
// use a unique specifier to import the module into Jint
var specifier = Guid.NewGuid().ToString();
var specifier = sourceUrl ?? Guid.NewGuid().ToString();

return ImportModule(specifier, source);
}
Expand All @@ -124,59 +118,52 @@ private JsValue LoadImportMap(String source)
{
var importMap = _engine.Evaluate($"JSON.parse('{source}')").AsObject();

// get list of imports based on any scoped imports for the current document path, and any global imports
var moduleImports = new Dictionary<string, string>();
var documentPathName = Url.Create(_documentUrl).PathName.ToLower();

if (importMap.TryGetValue("scopes", out var scopes))
{
var scopesObj = scopes.AsObject();

var scopePaths = scopesObj.GetOwnPropertyKeys().Select(k => k.AsString()).OrderByDescending(k => k.Length);

foreach (var scopePath in scopePaths)
foreach (var scopeProperty in scopesObj.GetOwnProperties())
{
if (!documentPathName.Contains(scopePath.ToLower()))
var scopePath = scopeProperty.Key.AsString();

if (_importMap.Scopes.ContainsKey(scopePath))
{
continue;
}

var scopeImports = scopesObj[scopePath].AsObject();
var scopeValue = new Dictionary<string, Uri>();

var scopeImportImportSpecifiers = scopeImports.GetOwnPropertyKeys().Select(k => k.AsString());
var scopeImports = scopesObj[scopePath].AsObject();

foreach (var scopeImportSpecifier in scopeImportImportSpecifiers)
foreach (var scopeImportProperty in scopeImports.GetOwnProperties())
{
if (!moduleImports.ContainsKey(scopeImportSpecifier))
var scopeImportSpecifier = scopeImportProperty.Key.AsString();

if (!scopeValue.ContainsKey(scopeImportSpecifier))
{
moduleImports.Add(scopeImportSpecifier, scopeImports[scopeImportSpecifier].AsString());
scopeValue.Add(scopeImportSpecifier, new Uri(scopeImports[scopeImportSpecifier].AsString(), UriKind.RelativeOrAbsolute));
}
}

_importMap.Scopes.Add(scopePath, scopeValue);
}
}

if (importMap.TryGetValue("imports", out var imports))
{
var importsObj = imports.AsObject();

var importSpecifiers = importsObj.GetOwnPropertyKeys().Select(k => k.AsString());

foreach (var importSpecifier in importSpecifiers)
foreach (var importProperty in importsObj.GetOwnProperties())
{
if (!moduleImports.ContainsKey(importSpecifier))
var importSpecifier = importProperty.Key.AsString();

if (!_importMap.Imports.ContainsKey(importSpecifier))
{
moduleImports.Add(importSpecifier, importsObj[importSpecifier].AsString());
_importMap.Imports.Add(importSpecifier, new Uri(importsObj[importSpecifier].AsString(), UriKind.RelativeOrAbsolute));
}
}
}

foreach (var import in moduleImports)
{
var moduleContent = FetchModule(new Uri(import.Value, UriKind.RelativeOrAbsolute));

ImportModule(import.Key, moduleContent);
}

return JsValue.Undefined;
}

Expand All @@ -188,34 +175,6 @@ private JsValue ImportModule(String specifier, String source)
return JsValue.Undefined;
}

public string FetchModule(Uri moduleUrl)
{
if (_resourceLoader == null)
{
return string.Empty;
}

if (!moduleUrl.IsAbsoluteUri)
{
moduleUrl = new Uri(new Uri(_documentUrl), moduleUrl);
}

var importUrl = Url.Convert(moduleUrl);

var request = new ResourceRequest(_scriptElement, importUrl);

var response = _resourceLoader.FetchAsync(request).Task.Result;

string content;

using (var streamReader = new StreamReader(response.Content))
{
content = streamReader.ReadToEnd();
}

return content;
}

#endregion

#region Helpers
Expand Down
8 changes: 4 additions & 4 deletions src/AngleSharp.Js/Extensions/EngineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,11 @@ public static void AddInstance(this EngineInstance engine, ObjectInstance obj, T
apply.Invoke(engine, obj);
}

public static JsValue RunScript(this EngineInstance engine, String source, String type) =>
engine.RunScript(source, type, engine.Window);
public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl) =>
engine.RunScript(source, type, sourceUrl, engine.Window);

public static JsValue RunScript(this EngineInstance engine, String source, String type, INode context) =>
engine.RunScript(source, type, context.ToJsValue(engine));
public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl, INode context) =>
engine.RunScript(source, type, sourceUrl, context.ToJsValue(engine));

public static JsValue Call(this EngineInstance instance, MethodInfo method, JsValue thisObject, JsValue[] arguments)
{
Expand Down
5 changes: 3 additions & 2 deletions src/AngleSharp.Js/JsApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ public static class JsApiExtensions
/// <param name="document">The document as context.</param>
/// <param name="scriptCode">The script to run.</param>
/// <param name="scriptType">The type of the script to run (defaults to "text/javascript").</param>
/// <param name="sourceUrl">The URL of the script.</param>
/// <returns>The result of running the script, if any.</returns>
public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null)
public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null, String sourceUrl = null)
{
if (document == null)
throw new ArgumentNullException(nameof(document));

var service = document?.Context.GetService<JsScriptingService>();
return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript);
return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript, sourceUrl);
}
}
}
29 changes: 29 additions & 0 deletions src/AngleSharp.Js/JsImportMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace AngleSharp.Js
{
using System;
using System.Collections.Generic;

/// <summary>
/// https://html.spec.whatwg.org/multipage/webappapis.html#import-map
/// </summary>
sealed class JsImportMap
{
public JsImportMap()
{
Imports = new Dictionary<string, Uri>();
Scopes = new Dictionary<string, Dictionary<string, Uri>>();
}

/// <summary>
/// Provides the mappings between module specifier text that might appear in an import statement or import() operator,
/// and the text that will replace it when the specifier is resolved.
/// </summary>
public Dictionary<string, Uri> Imports { get; set; }

/// <summary>
/// Mappings that are only used if the script importing the module contains a particular URL path.
/// If the URL of the loading script matches the supplied path, the mapping associated with the scope will be used.
/// </summary>
public Dictionary<string, Dictionary<string, Uri>> Scopes { get; set; }
}
}
Loading

0 comments on commit 11078c2

Please sign in to comment.