Skip to content

Commit c283d05

Browse files
authored
[Blazor] Consume fingerprinted assets in MVC and Blazor (#56045)
* Adds support for using fingerprinted assets in Blazor and MVC. * Fingerprinted assets are registered as additional endpoints to the system. * Static Assets exposes the descriptors so that they can be consumed and used by Blazor and MVC/Razor pages. * Helper methods `WithStaticAssets` map specific MapControllers/RazorPages/RazorComponents invocations to a given `MapStaticAsset` invocation and expose a `ResourceAssetCollection` as endpoint metadata that can be later on used by the system to change the way markup is rendered. * There's built-in support in all MVC Tag helpers (URLTagHelper, ImageTagHelper, LinkTagHelper, ScriptTagHelper). * There's support for JavaScript import maps in the ScriptTagHelper to render a JavaScript import map that maps JS modules to their fingerprinted URLs. * `ComponentBase` exposes an `Assets` property that lets Blazor components consume the fingerprinted names of a given asset. * When webassembly is in use, an additional couple of endpoints is registered exposing the mapping to webassembly as an additional JS Module that webassembly apps can use to consume the mapping from client code. * Caching is disabled during development for all assets to avoid issues and can be re-enabled through a configuration property. * Similarly, integrity is not exposed during development to avoid issues where users change a file on the fly while the app is running.
1 parent e558b05 commit c283d05

File tree

133 files changed

+5251
-397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

133 files changed

+5251
-397
lines changed

AspNetCore.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentInsider.Tests",
18161816
EndProject
18171817
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentSample", "src\Tools\GetDocumentInsider\sample\GetDocumentSample.csproj", "{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}"
18181818
EndProject
1819+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp.Client", "src\Components\Samples\BlazorUnitedApp.Client\BlazorUnitedApp.Client.csproj", "{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}"
1820+
EndProject
18191821
Global
18201822
GlobalSection(SolutionConfigurationPlatforms) = preSolution
18211823
Debug|Any CPU = Debug|Any CPU
@@ -10977,6 +10979,22 @@ Global
1097710979
{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x64.Build.0 = Release|Any CPU
1097810980
{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x86.ActiveCfg = Release|Any CPU
1097910981
{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x86.Build.0 = Release|Any CPU
10982+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10983+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
10984+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|arm64.ActiveCfg = Debug|Any CPU
10985+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|arm64.Build.0 = Debug|Any CPU
10986+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x64.ActiveCfg = Debug|Any CPU
10987+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x64.Build.0 = Debug|Any CPU
10988+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x86.ActiveCfg = Debug|Any CPU
10989+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x86.Build.0 = Debug|Any CPU
10990+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
10991+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|Any CPU.Build.0 = Release|Any CPU
10992+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|arm64.ActiveCfg = Release|Any CPU
10993+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|arm64.Build.0 = Release|Any CPU
10994+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x64.ActiveCfg = Release|Any CPU
10995+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x64.Build.0 = Release|Any CPU
10996+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x86.ActiveCfg = Release|Any CPU
10997+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x86.Build.0 = Release|Any CPU
1098010998
EndGlobalSection
1098110999
GlobalSection(SolutionProperties) = preSolution
1098211000
HideSolutionNode = FALSE
@@ -11874,6 +11892,7 @@ Global
1187411892
{9536C284-65B4-4884-BB50-06D629095C3E} = {274100A5-5B2D-4EA2-AC42-A62257FC6BDC}
1187511893
{6A19D94D-2BC6-4198-BE2E-342688FDBA4B} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0}
1187611894
{D8F7091E-A2D1-4E81-BA7C-97EAE392D683} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0}
11895+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
1187711896
EndGlobalSection
1187811897
GlobalSection(ExtensibilityGlobals) = postSolution
1187911898
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

src/Components/Components.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter\\src\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter.csproj",
2020
"src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid\\src\\Microsoft.AspNetCore.Components.QuickGrid.csproj",
2121
"src\\Components\\Samples\\BlazorServerApp\\BlazorServerApp.csproj",
22+
"src\\Components\\Samples\\BlazorUnitedApp.Client\\BlazorUnitedApp.Client.csproj",
2223
"src\\Components\\Samples\\BlazorUnitedApp\\BlazorUnitedApp.csproj",
2324
"src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
2425
"src\\Components\\Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj",

src/Components/Components/src/ComponentBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public ComponentBase()
4747
/// </summary>
4848
protected ComponentPlatform Platform => _renderHandle.Platform;
4949

50+
/// <summary>
51+
/// Gets the <see cref="ResourceAssetCollection"/> for the application.
52+
/// </summary>
53+
protected ResourceAssetCollection Assets => _renderHandle.Assets;
54+
5055
/// <summary>
5156
/// Gets the <see cref="IComponentRenderMode"/> assigned to this component.
5257
/// </summary>

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.ComponentBase.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection!
23
Microsoft.AspNetCore.Components.ComponentBase.AssignedRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode?
34
Microsoft.AspNetCore.Components.ComponentBase.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
45
Microsoft.AspNetCore.Components.ComponentPlatform
@@ -7,6 +8,21 @@ Microsoft.AspNetCore.Components.ComponentPlatform.IsInteractive.get -> bool
78
Microsoft.AspNetCore.Components.ComponentPlatform.Name.get -> string!
89
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute
910
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute.ExcludeFromInteractiveRoutingAttribute() -> void
11+
Microsoft.AspNetCore.Components.RenderHandle.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection!
1012
Microsoft.AspNetCore.Components.RenderHandle.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
1113
Microsoft.AspNetCore.Components.RenderHandle.RenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode?
14+
Microsoft.AspNetCore.Components.ResourceAsset
15+
Microsoft.AspNetCore.Components.ResourceAsset.Properties.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>?
16+
Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
17+
Microsoft.AspNetCore.Components.ResourceAsset.Url.get -> string!
18+
Microsoft.AspNetCore.Components.ResourceAssetCollection
19+
Microsoft.AspNetCore.Components.ResourceAssetCollection.IsContentSpecificUrl(string! path) -> bool
20+
Microsoft.AspNetCore.Components.ResourceAssetCollection.ResourceAssetCollection(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAsset!>! resources) -> void
21+
Microsoft.AspNetCore.Components.ResourceAssetCollection.this[string! key].get -> string!
22+
Microsoft.AspNetCore.Components.ResourceAssetProperty
23+
Microsoft.AspNetCore.Components.ResourceAssetProperty.Name.get -> string!
24+
Microsoft.AspNetCore.Components.ResourceAssetProperty.ResourceAssetProperty(string! name, string! value) -> void
25+
Microsoft.AspNetCore.Components.ResourceAssetProperty.Value.get -> string!
26+
static readonly Microsoft.AspNetCore.Components.ResourceAssetCollection.Empty -> Microsoft.AspNetCore.Components.ResourceAssetCollection!
27+
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection!
1228
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ComponentPlatform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!

src/Components/Components/src/RenderHandle.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ public IComponentRenderMode? RenderMode
7373
}
7474
}
7575

76+
/// <summary>
77+
/// Gets the <see cref="ResourceAssetCollection"/> associated with the <see cref="Renderer"/>.
78+
/// </summary>
79+
public ResourceAssetCollection Assets
80+
{
81+
get
82+
{
83+
return _renderer?.Assets ?? throw new InvalidOperationException("No renderer has been initialized.");
84+
}
85+
}
86+
7687
/// <summary>
7788
/// Notifies the renderer that the component should be rendered.
7889
/// </summary>

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ protected internal ComponentState GetComponentState(IComponent component)
158158
/// </summary>
159159
protected internal virtual ComponentPlatform ComponentPlatform { get; }
160160

161+
/// <summary>
162+
/// Gets the <see cref="ResourceAssetCollection"/> associated with this <see cref="Renderer"/>.
163+
/// </summary>
164+
protected internal virtual ResourceAssetCollection Assets { get; } = ResourceAssetCollection.Empty;
165+
161166
private async void RenderRootComponentsOnHotReload()
162167
{
163168
// Before re-rendering the root component, also clear any well-known caches in the framework
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Linq;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
/// <summary>
10+
/// A resource of the components application, such as a script, stylesheet or image.
11+
/// </summary>
12+
/// <param name="url">The URL of the resource.</param>
13+
/// <param name="properties">The properties associated to this resource.</param>
14+
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
15+
public sealed class ResourceAsset(string url, IReadOnlyList<ResourceAssetProperty>? properties)
16+
{
17+
/// <summary>
18+
/// Gets the URL that identifies this resource.
19+
/// </summary>
20+
public string Url { get; } = url;
21+
22+
/// <summary>
23+
/// Gets a list of properties associated to this resource.
24+
/// </summary>
25+
public IReadOnlyList<ResourceAssetProperty>? Properties { get; } = properties;
26+
27+
private string GetDebuggerDisplay() =>
28+
$"Url: '{Url}' - Properties: {string.Join(", ", Properties?.Select(p => $"{p.Name} = {p.Value}") ?? [])}";
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Frozen;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
/// <summary>
10+
/// Describes a mapping of static assets to their corresponding unique URLs.
11+
/// </summary>
12+
public sealed class ResourceAssetCollection : IReadOnlyList<ResourceAsset>
13+
{
14+
/// <summary>
15+
/// An empty <see cref="ResourceAssetCollection"/>.
16+
/// </summary>
17+
public static readonly ResourceAssetCollection Empty = new([]);
18+
19+
private readonly FrozenDictionary<string, ResourceAsset> _uniqueUrlMappings;
20+
private readonly FrozenSet<string> _contentSpecificUrls;
21+
private readonly IReadOnlyList<ResourceAsset> _resources;
22+
23+
/// <summary>
24+
/// Initializes a new instance of <see cref="ResourceAssetCollection"/>
25+
/// </summary>
26+
/// <param name="resources">The list of resources available.</param>
27+
public ResourceAssetCollection(IReadOnlyList<ResourceAsset> resources)
28+
{
29+
var mappings = new Dictionary<string, ResourceAsset>(StringComparer.OrdinalIgnoreCase);
30+
var contentSpecificUrls = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
31+
_resources = resources;
32+
foreach (var resource in resources)
33+
{
34+
foreach (var property in resource.Properties ?? [])
35+
{
36+
if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase))
37+
{
38+
if (mappings.TryGetValue(property.Value, out var value))
39+
{
40+
throw new InvalidOperationException($"The static asset '{property.Value}' is already mapped to {value.Url}.");
41+
}
42+
mappings[property.Value] = resource;
43+
contentSpecificUrls.Add(resource.Url);
44+
}
45+
}
46+
}
47+
48+
_uniqueUrlMappings = mappings.ToFrozenDictionary();
49+
_contentSpecificUrls = contentSpecificUrls.ToFrozenSet();
50+
}
51+
52+
/// <summary>
53+
/// Gets the unique content-based URL for the specified static asset.
54+
/// </summary>
55+
/// <param name="key">The asset name.</param>
56+
/// <returns>The unique URL if availabe, the same <paramref name="key"/> if not available.</returns>
57+
public string this[string key] => _uniqueUrlMappings.TryGetValue(key, out var value) ? value.Url : key;
58+
59+
/// <summary>
60+
/// Determines whether the specified path is a content-specific URL.
61+
/// </summary>
62+
/// <param name="path">The path to check.</param>
63+
/// <returns><c>true</c> if the path is a content-specific URL; otherwise, <c>false</c>.</returns>
64+
public bool IsContentSpecificUrl(string path) => _contentSpecificUrls.Contains(path);
65+
66+
// IReadOnlyList<ResourceAsset> implementation
67+
ResourceAsset IReadOnlyList<ResourceAsset>.this[int index] => _resources[index];
68+
int IReadOnlyCollection<ResourceAsset>.Count => _resources.Count;
69+
IEnumerator<ResourceAsset> IEnumerable<ResourceAsset>.GetEnumerator() => _resources.GetEnumerator();
70+
IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator();
71+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// A resource property.
8+
/// </summary>
9+
/// <param name="name">The name of the property.</param>
10+
/// <param name="value">The value of the property.</param>
11+
public sealed class ResourceAssetProperty(string name, string value)
12+
{
13+
/// <summary>
14+
/// Gets the name of the property.
15+
/// </summary>
16+
public string Name { get; } = name;
17+
18+
/// <summary>
19+
/// Gets the value of the property.
20+
/// </summary>
21+
public string Value { get; } = value;
22+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
public class ResourceAssetCollectionTest
7+
{
8+
[Fact]
9+
public void CanCreateResourceCollection()
10+
{
11+
// Arrange
12+
var resourceAssetCollection = new ResourceAssetCollection([
13+
new ResourceAsset("image1.jpg",[]),
14+
]);
15+
16+
// Act
17+
var collectionAsReadOnlyList = resourceAssetCollection as IReadOnlyList<ResourceAsset>;
18+
19+
// Assert
20+
Assert.Equal(1, collectionAsReadOnlyList.Count);
21+
Assert.Equal("image1.jpg", collectionAsReadOnlyList[0].Url);
22+
}
23+
24+
[Fact]
25+
public void CanResolveFingerprintedResources()
26+
{
27+
// Arrange
28+
var resourceAssetCollection = new ResourceAssetCollection([
29+
new ResourceAsset(
30+
"image1.fingerprint.jpg",
31+
[new ResourceAssetProperty("label", "image1.jpg")]),
32+
]);
33+
34+
// Act
35+
var resolvedUrl = resourceAssetCollection["image1.jpg"];
36+
37+
// Assert
38+
Assert.Equal("image1.fingerprint.jpg", resolvedUrl);
39+
}
40+
41+
[Fact]
42+
public void ResolvingNoFingerprintedResourcesReturnsSameUrl()
43+
{
44+
// Arrange
45+
var resourceAssetCollection = new ResourceAssetCollection([
46+
new ResourceAsset("image1.jpg",[])]);
47+
48+
// Act
49+
var resolvedUrl = resourceAssetCollection["image1.jpg"];
50+
51+
// Assert
52+
Assert.Equal("image1.jpg", resolvedUrl);
53+
}
54+
55+
[Fact]
56+
public void ResolvingNonExistentResourceReturnsSameUrl()
57+
{
58+
// Arrange
59+
var resourceAssetCollection = new ResourceAssetCollection([
60+
new ResourceAsset("image1.jpg",[])]);
61+
62+
// Act
63+
var resolvedUrl = resourceAssetCollection["image2.jpg"];
64+
65+
// Assert
66+
Assert.Equal("image2.jpg", resolvedUrl);
67+
}
68+
69+
[Fact]
70+
public void CanDetermineContentSpecificUrls()
71+
{
72+
// Arrange
73+
var resourceAssetCollection = new ResourceAssetCollection([
74+
new ResourceAsset("image1.jpg",[]),
75+
new ResourceAsset(
76+
"image2.fingerprint.jpg",
77+
[new ResourceAssetProperty("label", "image2.jpg")]),
78+
]);
79+
80+
// Act
81+
var isContentSpecificUrl1 = resourceAssetCollection.IsContentSpecificUrl("image1.jpg");
82+
var isContentSpecificUrl2 = resourceAssetCollection.IsContentSpecificUrl("image2.fingerprint.jpg");
83+
84+
// Assert
85+
Assert.False(isContentSpecificUrl1);
86+
Assert.True(isContentSpecificUrl2);
87+
}
88+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
/// <summary>
10+
/// Represents an <c><script type="importmap"></script></c> element that defines the import map for module scripts
11+
/// in the application.
12+
/// </summary>
13+
public sealed class ImportMap : IComponent
14+
{
15+
private RenderHandle _renderHandle;
16+
private bool _firstRender = true;
17+
private ImportMapDefinition? _computedImportMapDefinition;
18+
19+
/// <summary>
20+
/// Gets or sets the <see cref="HttpContext"/> for the component.
21+
/// </summary>
22+
[CascadingParameter] public HttpContext? HttpContext { get; set; } = null;
23+
24+
/// <summary>
25+
/// Gets or sets the import map definition to use for the component. If not set
26+
/// the component will generate the import map based on the assets defined for this
27+
/// application.
28+
/// </summary>
29+
[Parameter]
30+
public ImportMapDefinition? ImportMapDefinition { get; set; }
31+
32+
void IComponent.Attach(RenderHandle renderHandle)
33+
{
34+
_renderHandle = renderHandle;
35+
}
36+
37+
Task IComponent.SetParametersAsync(ParameterView parameters)
38+
{
39+
parameters.SetParameterProperties(this);
40+
if (!_firstRender && ReferenceEquals(ImportMapDefinition, _computedImportMapDefinition))
41+
{
42+
return Task.CompletedTask;
43+
}
44+
else
45+
{
46+
_firstRender = false;
47+
_computedImportMapDefinition = ImportMapDefinition ?? HttpContext?.GetEndpoint()?.Metadata.GetMetadata<ImportMapDefinition>();
48+
if (_computedImportMapDefinition != null)
49+
{
50+
_renderHandle.Render(RenderImportMap);
51+
}
52+
return Task.CompletedTask;
53+
}
54+
}
55+
56+
private void RenderImportMap(RenderTreeBuilder builder)
57+
{
58+
builder.OpenElement(0, "script");
59+
builder.AddAttribute(1, "type", "importmap");
60+
builder.AddMarkupContent(2, _computedImportMapDefinition!.ToJson());
61+
builder.CloseElement();
62+
}
63+
}

0 commit comments

Comments
 (0)