Skip to content

Commit 11078c2

Browse files
Refactored importmap scopes
1 parent 9b59f35 commit 11078c2

8 files changed

+182
-94
lines changed

src/AngleSharp.Js.Tests/EcmaTests.cs

+24-3
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,40 @@ public async Task ModuleScriptWithScopedImportMapShouldRunCorrectScript()
8484
{
8585
{ "/example-module-1.js", "export function test() { document.getElementById('test1').remove(); }" },
8686
{ "/example-module-2.js", "export function test() { document.getElementById('test2').remove(); }" },
87+
{ "/test.js", "import { test } from 'example-module'; test();" },
88+
{ "/test/test.js", "import { test } from 'example-module'; test();" }
8789
}))
8890
.WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });
8991

9092
var context = BrowsingContext.New(config);
91-
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>";
9293

93-
var document1 = await context.OpenAsync(r => r.Content(html));
94+
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>";
95+
var document1 = await context.OpenAsync(r => r.Content(html1));
9496
Assert.IsNull(document1.GetElementById("test1"));
9597
Assert.IsNotNull(document1.GetElementById("test2"));
9698

97-
var document2 = await context.OpenAsync(r => r.Content(html).Address("http://localhost/test/"));
99+
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>";
100+
var document2 = await context.OpenAsync(r => r.Content(html2));
98101
Assert.IsNull(document2.GetElementById("test2"));
99102
Assert.IsNotNull(document2.GetElementById("test1"));
100103
}
104+
105+
[Test]
106+
public async Task ModuleScriptWithAbsoluteUrlImportMapShouldRun()
107+
{
108+
var config =
109+
Configuration.Default
110+
.WithJs()
111+
.With(new MockHttpClientRequester(new Dictionary<string, string>()
112+
{
113+
{ "/jquery_4_0_0_esm.js", Constants.Jquery4_0_0_ESM }
114+
}))
115+
.WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });
116+
117+
var context = BrowsingContext.New(config);
118+
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>";
119+
var document = await context.OpenAsync(r => r.Content(html));
120+
Assert.IsNull(document.GetElementById("test"));
121+
}
101122
}
102123
}

src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
using AngleSharp.Io;
2-
using AngleSharp.Io.Network;
3-
using System.Collections.Generic;
4-
using System.IO;
5-
using System.Net;
6-
using System.Text;
7-
using System.Threading;
8-
using System.Threading.Tasks;
9-
101
namespace AngleSharp.Js.Tests.Mocks
112
{
3+
using AngleSharp.Io;
4+
using AngleSharp.Io.Network;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Net;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
1212
/// <summary>
1313
/// Mock HttpClientRequester which returns content for a specific request from a local dictionary.
1414
/// </summary>

src/AngleSharp.Js/EngineInstance.cs

+25-66
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ namespace AngleSharp.Js
88
using Jint.Native.Object;
99
using System;
1010
using System.Collections.Generic;
11-
using System.IO;
12-
using System.Linq;
1311
using System.Reflection;
1412

1513
sealed class EngineInstance
@@ -21,25 +19,19 @@ sealed class EngineInstance
2119
private readonly ReferenceCache _references;
2220
private readonly IEnumerable<Assembly> _libs;
2321
private readonly DomNodeInstance _window;
24-
private readonly IResourceLoader _resourceLoader;
25-
private readonly IElement _scriptElement;
26-
private readonly string _documentUrl;
22+
private readonly JsImportMap _importMap;
2723

2824
#endregion
2925

3026
#region ctor
3127

3228
public EngineInstance(IWindow window, IDictionary<String, Object> assignments, IEnumerable<Assembly> libs)
3329
{
34-
_resourceLoader = window.Document.Context.GetService<IResourceLoader>();
35-
36-
_scriptElement = window.Document.CreateElement(TagNames.Script);
37-
38-
_documentUrl = window.Document.Url;
30+
_importMap = new JsImportMap();
3931

4032
_engine = new Engine((options) =>
4133
{
42-
options.EnableModules(new JsModuleLoader(this, _documentUrl, false));
34+
options.EnableModules(new JsModuleLoader(this, window.Document, false));
4335
});
4436
_prototypes = new PrototypeCache(_engine);
4537
_references = new ReferenceCache();
@@ -81,6 +73,8 @@ public EngineInstance(IWindow window, IDictionary<String, Object> assignments, I
8173

8274
public Engine Jint => _engine;
8375

76+
public JsImportMap ImportMap => _importMap;
77+
8478
#endregion
8579

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

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

92-
public JsValue RunScript(String source, String type, JsValue context)
86+
public JsValue RunScript(String source, String type, String sourceUrl, JsValue context)
9387
{
9488
if (string.IsNullOrEmpty(type))
9589
{
@@ -109,7 +103,7 @@ public JsValue RunScript(String source, String type, JsValue context)
109103
else if (type.Isi("module"))
110104
{
111105
// use a unique specifier to import the module into Jint
112-
var specifier = Guid.NewGuid().ToString();
106+
var specifier = sourceUrl ?? Guid.NewGuid().ToString();
113107

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

127-
// get list of imports based on any scoped imports for the current document path, and any global imports
128-
var moduleImports = new Dictionary<string, string>();
129-
var documentPathName = Url.Create(_documentUrl).PathName.ToLower();
130-
131121
if (importMap.TryGetValue("scopes", out var scopes))
132122
{
133123
var scopesObj = scopes.AsObject();
134124

135-
var scopePaths = scopesObj.GetOwnPropertyKeys().Select(k => k.AsString()).OrderByDescending(k => k.Length);
136-
137-
foreach (var scopePath in scopePaths)
125+
foreach (var scopeProperty in scopesObj.GetOwnProperties())
138126
{
139-
if (!documentPathName.Contains(scopePath.ToLower()))
127+
var scopePath = scopeProperty.Key.AsString();
128+
129+
if (_importMap.Scopes.ContainsKey(scopePath))
140130
{
141131
continue;
142132
}
143133

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

146-
var scopeImportImportSpecifiers = scopeImports.GetOwnPropertyKeys().Select(k => k.AsString());
136+
var scopeImports = scopesObj[scopePath].AsObject();
147137

148-
foreach (var scopeImportSpecifier in scopeImportImportSpecifiers)
138+
foreach (var scopeImportProperty in scopeImports.GetOwnProperties())
149139
{
150-
if (!moduleImports.ContainsKey(scopeImportSpecifier))
140+
var scopeImportSpecifier = scopeImportProperty.Key.AsString();
141+
142+
if (!scopeValue.ContainsKey(scopeImportSpecifier))
151143
{
152-
moduleImports.Add(scopeImportSpecifier, scopeImports[scopeImportSpecifier].AsString());
144+
scopeValue.Add(scopeImportSpecifier, new Uri(scopeImports[scopeImportSpecifier].AsString(), UriKind.RelativeOrAbsolute));
153145
}
154146
}
147+
148+
_importMap.Scopes.Add(scopePath, scopeValue);
155149
}
156150
}
157151

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

162-
var importSpecifiers = importsObj.GetOwnPropertyKeys().Select(k => k.AsString());
163-
164-
foreach (var importSpecifier in importSpecifiers)
156+
foreach (var importProperty in importsObj.GetOwnProperties())
165157
{
166-
if (!moduleImports.ContainsKey(importSpecifier))
158+
var importSpecifier = importProperty.Key.AsString();
159+
160+
if (!_importMap.Imports.ContainsKey(importSpecifier))
167161
{
168-
moduleImports.Add(importSpecifier, importsObj[importSpecifier].AsString());
162+
_importMap.Imports.Add(importSpecifier, new Uri(importsObj[importSpecifier].AsString(), UriKind.RelativeOrAbsolute));
169163
}
170164
}
171165
}
172166

173-
foreach (var import in moduleImports)
174-
{
175-
var moduleContent = FetchModule(new Uri(import.Value, UriKind.RelativeOrAbsolute));
176-
177-
ImportModule(import.Key, moduleContent);
178-
}
179-
180167
return JsValue.Undefined;
181168
}
182169

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

191-
public string FetchModule(Uri moduleUrl)
192-
{
193-
if (_resourceLoader == null)
194-
{
195-
return string.Empty;
196-
}
197-
198-
if (!moduleUrl.IsAbsoluteUri)
199-
{
200-
moduleUrl = new Uri(new Uri(_documentUrl), moduleUrl);
201-
}
202-
203-
var importUrl = Url.Convert(moduleUrl);
204-
205-
var request = new ResourceRequest(_scriptElement, importUrl);
206-
207-
var response = _resourceLoader.FetchAsync(request).Task.Result;
208-
209-
string content;
210-
211-
using (var streamReader = new StreamReader(response.Content))
212-
{
213-
content = streamReader.ReadToEnd();
214-
}
215-
216-
return content;
217-
}
218-
219178
#endregion
220179

221180
#region Helpers

src/AngleSharp.Js/Extensions/EngineExtensions.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,11 @@ public static void AddInstance(this EngineInstance engine, ObjectInstance obj, T
198198
apply.Invoke(engine, obj);
199199
}
200200

201-
public static JsValue RunScript(this EngineInstance engine, String source, String type) =>
202-
engine.RunScript(source, type, engine.Window);
201+
public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl) =>
202+
engine.RunScript(source, type, sourceUrl, engine.Window);
203203

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

207207
public static JsValue Call(this EngineInstance instance, MethodInfo method, JsValue thisObject, JsValue[] arguments)
208208
{

src/AngleSharp.Js/JsApiExtensions.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ public static class JsApiExtensions
1616
/// <param name="document">The document as context.</param>
1717
/// <param name="scriptCode">The script to run.</param>
1818
/// <param name="scriptType">The type of the script to run (defaults to "text/javascript").</param>
19+
/// <param name="sourceUrl">The URL of the script.</param>
1920
/// <returns>The result of running the script, if any.</returns>
20-
public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null)
21+
public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null, String sourceUrl = null)
2122
{
2223
if (document == null)
2324
throw new ArgumentNullException(nameof(document));
2425

2526
var service = document?.Context.GetService<JsScriptingService>();
26-
return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript);
27+
return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript, sourceUrl);
2728
}
2829
}
2930
}

src/AngleSharp.Js/JsImportMap.cs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace AngleSharp.Js
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
6+
/// <summary>
7+
/// https://html.spec.whatwg.org/multipage/webappapis.html#import-map
8+
/// </summary>
9+
sealed class JsImportMap
10+
{
11+
public JsImportMap()
12+
{
13+
Imports = new Dictionary<string, Uri>();
14+
Scopes = new Dictionary<string, Dictionary<string, Uri>>();
15+
}
16+
17+
/// <summary>
18+
/// Provides the mappings between module specifier text that might appear in an import statement or import() operator,
19+
/// and the text that will replace it when the specifier is resolved.
20+
/// </summary>
21+
public Dictionary<string, Uri> Imports { get; set; }
22+
23+
/// <summary>
24+
/// Mappings that are only used if the script importing the module contains a particular URL path.
25+
/// If the URL of the loading script matches the supplied path, the mapping associated with the scope will be used.
26+
/// </summary>
27+
public Dictionary<string, Dictionary<string, Uri>> Scopes { get; set; }
28+
}
29+
}

0 commit comments

Comments
 (0)