diff --git a/src/DefaultBuilder/ref/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/ref/Microsoft.AspNetCore.csproj
index 12795e2033..dd42476a01 100644
--- a/src/DefaultBuilder/ref/Microsoft.AspNetCore.csproj
+++ b/src/DefaultBuilder/ref/Microsoft.AspNetCore.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/DefaultBuilder/ref/Microsoft.AspNetCore.netcoreapp3.0.cs b/src/DefaultBuilder/ref/Microsoft.AspNetCore.netcoreapp3.0.cs
index 1737054dc0..e5a76886a4 100644
--- a/src/DefaultBuilder/ref/Microsoft.AspNetCore.netcoreapp3.0.cs
+++ b/src/DefaultBuilder/ref/Microsoft.AspNetCore.netcoreapp3.0.cs
@@ -3,6 +3,10 @@
namespace Microsoft.AspNetCore
{
+ public static partial class StaticWebAssetsWebHostBuilderExtensions
+ {
+ public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseStaticWebAssets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) { throw null; }
+ }
public static partial class WebHost
{
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder CreateDefaultBuilder() { throw null; }
diff --git a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
index d02c88f673..76a27b9a71 100644
--- a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
+++ b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/DefaultBuilder/src/Properties/AssemblyInfo.cs b/src/DefaultBuilder/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..97ac339e54
--- /dev/null
+++ b/src/DefaultBuilder/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/DefaultBuilder/src/StaticWebAssetsFileProvider.cs b/src/DefaultBuilder/src/StaticWebAssetsFileProvider.cs
new file mode 100644
index 0000000000..99919f1a1c
--- /dev/null
+++ b/src/DefaultBuilder/src/StaticWebAssetsFileProvider.cs
@@ -0,0 +1,77 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore
+{
+ // A file provider used for serving static web assets from referenced projects and packages during development.
+ // The file provider maps folders from referenced projects and packages and prepends a prefix to their relative
+ // paths.
+ // At publish time the assets end up in the wwwroot folder of the published app under the prefix indicated here
+ // as the base path.
+ // For example, for a referenced project mylibrary with content under <>\wwwroot will expose
+ // static web assets under _content/mylibrary (this is by convention). The path prefix or base path we apply
+ // is that (_content/mylibrary).
+ // when the app gets published, the build pipeline puts the static web assets for mylibrary under
+ // publish/wwwroot/_content/mylibrary/sample-asset.js
+ // To allow for the same experience during development, StaticWebAssetsFileProvider maps the contents of
+ // <>\wwwroot\** to _content/mylibrary/**
+ internal class StaticWebAssetsFileProvider : IFileProvider
+ {
+ private static readonly StringComparison FilePathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
+ StringComparison.OrdinalIgnoreCase :
+ StringComparison.Ordinal;
+
+ public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot)
+ {
+ BasePath = new PathString(pathPrefix.StartsWith("/") ? pathPrefix : "/" + pathPrefix);
+ InnerProvider = new PhysicalFileProvider(contentRoot);
+ }
+
+ public PhysicalFileProvider InnerProvider { get; }
+
+ public PathString BasePath { get; }
+
+ ///
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ if (!StartsWithBasePath(subpath, out var physicalPath))
+ {
+ return NotFoundDirectoryContents.Singleton;
+ }
+ else
+ {
+ return InnerProvider.GetDirectoryContents(physicalPath);
+ }
+ }
+
+ ///
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ if (!StartsWithBasePath(subpath, out var physicalPath))
+ {
+ return new NotFoundFileInfo(subpath);
+ }
+ else
+ {
+ return InnerProvider.GetFileInfo(physicalPath);
+ }
+ }
+
+ ///
+ public IChangeToken Watch(string filter)
+ {
+ return InnerProvider.Watch(filter);
+ }
+
+ private bool StartsWithBasePath(string subpath, out PathString rest)
+ {
+ return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest);
+ }
+ }
+}
diff --git a/src/DefaultBuilder/src/StaticWebAssetsLoader.cs b/src/DefaultBuilder/src/StaticWebAssetsLoader.cs
new file mode 100644
index 0000000000..05ee714f0c
--- /dev/null
+++ b/src/DefaultBuilder/src/StaticWebAssetsLoader.cs
@@ -0,0 +1,96 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore
+{
+ internal class StaticWebAssetsLoader
+ {
+ internal const string StaticWebAssetsManifestName = "Microsoft.AspNetCore.StaticWebAssets.xml";
+
+ internal static void UseStaticWebAssets(IWebHostEnvironment environment)
+ {
+ using (var manifest = ResolveManifest(environment))
+ {
+ if (manifest != null)
+ {
+ UseStaticWebAssetsCore(environment, manifest);
+ }
+ }
+ }
+
+ internal static void UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest)
+ {
+ var staticWebAssetsFileProvider = new List();
+ var webRootFileProvider = environment.WebRootFileProvider;
+
+ var additionalFiles = StaticWebAssetsReader.Parse(manifest)
+ .Select(cr => new StaticWebAssetsFileProvider(cr.BasePath, cr.Path))
+ .OfType() // Upcast so we can insert on the resulting list.
+ .ToList();
+
+ if (additionalFiles.Count == 0)
+ {
+ return;
+ }
+ else
+ {
+ additionalFiles.Insert(0, webRootFileProvider);
+ environment.WebRootFileProvider = new CompositeFileProvider(additionalFiles);
+ }
+ }
+
+ internal static Stream ResolveManifest(IWebHostEnvironment environment)
+ {
+ // We plan to remove the embedded file resolution code path in
+ // a future preview.
+ Assembly assembly = null;
+ try
+ {
+ assembly = Assembly.Load(environment.ApplicationName);
+ }
+ catch (Exception)
+ {
+ }
+
+ if (assembly != null && assembly.GetManifestResourceNames().Any(a => a == StaticWebAssetsManifestName))
+ {
+ return assembly.GetManifestResourceStream(StaticWebAssetsManifestName);
+ }
+ else
+ {
+ // Fallback to physical file as we plan to use a file on disk instead of the embedded resource.
+ var filePath = Path.Combine(Path.GetDirectoryName(GetAssemblyLocation(assembly)), $"{environment.ApplicationName}.StaticWebAssets.xml");
+ if (File.Exists(filePath))
+ {
+ return File.OpenRead(filePath);
+ }
+ else
+ {
+ // A missing manifest might simply mean that the feature is not enabled, so we simply
+ // return early. Misconfigurations will be uncommon given that the entire process is automated
+ // at build time.
+ return null;
+ }
+ }
+ }
+
+ internal static string GetAssemblyLocation(Assembly assembly)
+ {
+ if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out var result) &&
+ result.IsFile && string.IsNullOrWhiteSpace(result.Fragment))
+ {
+ return result.LocalPath;
+ }
+
+ return assembly.Location;
+ }
+ }
+}
diff --git a/src/DefaultBuilder/src/StaticWebAssetsReader.cs b/src/DefaultBuilder/src/StaticWebAssetsReader.cs
new file mode 100644
index 0000000000..82ec626945
--- /dev/null
+++ b/src/DefaultBuilder/src/StaticWebAssetsReader.cs
@@ -0,0 +1,75 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore
+{
+ internal static class StaticWebAssetsReader
+ {
+ private const string ManifestRootElementName = "StaticWebAssets";
+ private const string VersionAttributeName = "Version";
+ private const string ContentRootElementName = "ContentRoot";
+
+ internal static IEnumerable Parse(Stream manifest)
+ {
+ var document = XDocument.Load(manifest);
+ if (!string.Equals(document.Root.Name.LocalName, ManifestRootElementName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"Invalid manifest format. Manifest root must be '{ManifestRootElementName}'");
+ }
+
+ var version = document.Root.Attribute(VersionAttributeName);
+ if (version == null)
+ {
+ throw new InvalidOperationException($"Invalid manifest format. Manifest root element must contain a version '{VersionAttributeName}' attribute");
+ }
+
+ if (version.Value != "1.0")
+ {
+ throw new InvalidOperationException($"Unknown manifest version. Manifest version must be '1.0'");
+ }
+
+ foreach (var element in document.Root.Elements())
+ {
+ if (!string.Equals(element.Name.LocalName, ContentRootElementName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"Invalid manifest format. Invalid element '{element.Name.LocalName}'. All {StaticWebAssetsLoader.StaticWebAssetsManifestName} child elements must be '{ContentRootElementName}' elements.");
+ }
+ if (!element.IsEmpty)
+ {
+ throw new InvalidOperationException($"Invalid manifest format. {ContentRootElementName} can't have content.");
+ }
+
+ var basePath = ParseRequiredAttribute(element, "BasePath");
+ var path = ParseRequiredAttribute(element, "Path");
+ yield return new ContentRootMapping(basePath, path);
+ }
+ }
+
+ private static string ParseRequiredAttribute(XElement element, string attributeName)
+ {
+ var attribute = element.Attribute(attributeName);
+ if (attribute == null)
+ {
+ throw new InvalidOperationException($"Invalid manifest format. Missing {attributeName} attribute in '{ContentRootElementName}' element.");
+ }
+ return attribute.Value;
+ }
+
+ internal readonly struct ContentRootMapping
+ {
+ public ContentRootMapping(string basePath, string path)
+ {
+ BasePath = basePath;
+ Path = path;
+ }
+
+ public string BasePath { get; }
+ public string Path { get; }
+ }
+ }
+}
diff --git a/src/DefaultBuilder/src/StaticWebAssetsWebHostBuilderExtensions.cs b/src/DefaultBuilder/src/StaticWebAssetsWebHostBuilderExtensions.cs
new file mode 100644
index 0000000000..8a754df151
--- /dev/null
+++ b/src/DefaultBuilder/src/StaticWebAssetsWebHostBuilderExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Hosting;
+
+namespace Microsoft.AspNetCore
+{
+ ///
+ /// Extensions for configuring static web assets for development.
+ ///
+ public static class StaticWebAssetsWebHostBuilderExtensions
+ {
+ ///
+ /// Configures the to use static web assets
+ /// defined by referenced projects and packages.
+ ///
+ /// The .
+ /// The .
+ public static IWebHostBuilder UseStaticWebAssets(this IWebHostBuilder builder)
+ {
+ builder.ConfigureAppConfiguration((context, configBuilder) =>
+ {
+ StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment);
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs
index e335931042..5e315dc5d1 100644
--- a/src/DefaultBuilder/src/WebHost.cs
+++ b/src/DefaultBuilder/src/WebHost.cs
@@ -206,6 +206,13 @@ namespace Microsoft.AspNetCore
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
+ builder.ConfigureAppConfiguration((ctx, cb) =>
+ {
+ if (ctx.HostingEnvironment.IsDevelopment())
+ {
+ StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment);
+ }
+ });
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"));
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml
new file mode 100644
index 0000000000..f10b7c1a1d
--- /dev/null
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj
index d23ae11780..6a1ff8c07d 100644
--- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj
@@ -1,4 +1,4 @@
-
+
netcoreapp3.0
@@ -7,6 +7,8 @@
+
+
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsFileProviderTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsFileProviderTests.cs
new file mode 100644
index 0000000000..f0066ea305
--- /dev/null
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsFileProviderTests.cs
@@ -0,0 +1,88 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Tests
+{
+ public class StaticWebAssetsFileProviderTests
+ {
+ [Fact]
+ public void StaticWebAssetsFileProvider_ConstructorThrows_WhenPathIsNotFound()
+ {
+ // Arrange, Act & Assert
+ var provider = Assert.Throws(() => new StaticWebAssetsFileProvider("/prefix", "/nonexisting"));
+ }
+
+ [Fact]
+ public void StaticWebAssetsFileProvider_Constructor_PrependsPrefixWithSlashIfMissing()
+ {
+ // Arrange & Act
+ var provider = new StaticWebAssetsFileProvider("_content", AppContext.BaseDirectory);
+
+ // Assert
+ Assert.Equal("/_content", provider.BasePath);
+ }
+
+ [Fact]
+ public void StaticWebAssetsFileProvider_Constructor_DoesNotPrependPrefixWithSlashIfPresent()
+ {
+ // Arrange & Act
+ var provider = new StaticWebAssetsFileProvider("/_content", AppContext.BaseDirectory);
+
+ // Assert
+ Assert.Equal("/_content", provider.BasePath);
+ }
+
+ [Fact]
+ public void GetFileInfo_DoesNotMatch_IncompletePrefixSegments()
+ {
+ // Arrange
+ var expectedResult = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var provider = new StaticWebAssetsFileProvider(
+ "_cont",
+ Path.GetDirectoryName(new Uri(typeof(StaticWebAssetsFileProviderTests).Assembly.CodeBase).LocalPath));
+
+ // Act
+ var file = provider.GetFileInfo("/_content/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml");
+
+ // Assert
+ Assert.False(file.Exists, "File exists");
+ }
+
+ [Fact]
+ public void GetFileInfo_Prefix_RespectsOsCaseSensitivity()
+ {
+ // Arrange
+ var expectedResult = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var provider = new StaticWebAssetsFileProvider(
+ "_content",
+ Path.GetDirectoryName(new Uri(typeof(StaticWebAssetsFileProviderTests).Assembly.CodeBase).LocalPath));
+
+ // Act
+ var file = provider.GetFileInfo("/_CONTENT/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml");
+
+ // Assert
+ Assert.Equal(expectedResult, file.Exists);
+ }
+
+ [Fact]
+ public void GetDirectoryContents_Prefix_RespectsOsCaseSensitivity()
+ {
+ // Arrange
+ var expectedResult = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var provider = new StaticWebAssetsFileProvider(
+ "_content",
+ Path.GetDirectoryName(new Uri(typeof(StaticWebAssetsFileProviderTests).Assembly.CodeBase).LocalPath));
+
+ // Act
+ var directory = provider.GetDirectoryContents("/_CONTENT");
+
+ // Assert
+ Assert.Equal(expectedResult, directory.Exists);
+ }
+ }
+}
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsLoaderTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsLoaderTests.cs
new file mode 100644
index 0000000000..bed656a259
--- /dev/null
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsLoaderTests.cs
@@ -0,0 +1,108 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.AspNetCore.Hosting.Internal;
+using Microsoft.Extensions.FileProviders;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Tests
+{
+ public class StaticWebAssetsLoaderTests
+ {
+ [Fact]
+ public void UseStaticWebAssetsCore_CreatesCompositeRoot_WhenThereAreContentRootsInTheManifest()
+ {
+ // Arrange
+ var manifestContent = @$"
+
+";
+
+ var manifest = CreateManifest(manifestContent);
+ var originalRoot = new NullFileProvider();
+ var environment = new HostingEnvironment()
+ {
+ WebRootFileProvider = originalRoot
+ };
+
+ // Act
+ StaticWebAssetsLoader.UseStaticWebAssetsCore(environment, manifest);
+
+ // Assert
+ var composite = Assert.IsType(environment.WebRootFileProvider);
+ Assert.Equal(2, composite.FileProviders.Count());
+ Assert.Equal(originalRoot, composite.FileProviders.First());
+ }
+
+ [Fact]
+ public void UseStaticWebAssetsCore_DoesNothing_WhenManifestDoesNotContainEntries()
+ {
+ // Arrange
+ var manifestContent = @$"
+";
+
+ var manifest = CreateManifest(manifestContent);
+ var originalRoot = new NullFileProvider();
+ var environment = new HostingEnvironment()
+ {
+ WebRootFileProvider = originalRoot
+ };
+
+ // Act
+ StaticWebAssetsLoader.UseStaticWebAssetsCore(environment, manifest);
+
+ // Assert
+ Assert.Equal(originalRoot, environment.WebRootFileProvider);
+ }
+
+ [Fact]
+ public void ResolveManifest_FindsEmbeddedManifestProvider()
+ {
+ // Arrange
+ var expectedManifest = @"
+
+
+";
+ var originalRoot = new NullFileProvider();
+ var environment = new HostingEnvironment()
+ {
+ ApplicationName = typeof(StaticWebAssetsReaderTests).Assembly.GetName().Name
+ };
+
+ // Act
+ var manifest = StaticWebAssetsLoader.ResolveManifest(environment);
+
+ // Assert
+ Assert.Equal(expectedManifest, new StreamReader(manifest).ReadToEnd());
+ }
+
+ [Fact]
+ public void ResolveManifest_ManifestFromFile()
+ {
+ // Arrange
+ var expectedManifest = @"
+
+
+";
+
+ var environment = new HostingEnvironment()
+ {
+ ApplicationName = "Microsoft.AspNetCore.TestHost"
+ };
+
+ // Act
+ var manifest = StaticWebAssetsLoader.ResolveManifest(environment);
+
+ // Assert
+ Assert.Equal(expectedManifest, new StreamReader(manifest).ReadToEnd());
+ }
+
+ private Stream CreateManifest(string manifestContent)
+ {
+ return new MemoryStream(Encoding.UTF8.GetBytes(manifestContent));
+ }
+ }
+}
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsReaderTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsReaderTests.cs
new file mode 100644
index 0000000000..1adffad46c
--- /dev/null
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/StaticWebAssets/StaticWebAssetsReaderTests.cs
@@ -0,0 +1,147 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Tests
+{
+ public class StaticWebAssetsReaderTests
+ {
+ [Fact]
+ public void ParseManifest_ThrowsFor_EmptyManifest()
+ {
+ // Arrange
+ var manifestContent = @"";
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Root element is missing.", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_UnknownRootElement()
+ {
+ // Arrange
+ var manifestContent = @"";
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_MissingVersion()
+ {
+ // Arrange
+ var manifestContent = @"";
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_UnknownVersion()
+ {
+ // Arrange
+ var manifestContent = @"";
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Unknown manifest version", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_InvalidStaticWebAssetsChildren()
+ {
+ // Arrange
+ var manifestContent = @"
+
+";
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_MissingBasePath()
+ {
+ // Arrange
+ var manifestContent = @"
+
+";
+
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_MissingPath()
+ {
+ // Arrange
+ var manifestContent = @"
+
+";
+
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ThrowsFor_ChildContentRootContent()
+ {
+ // Arrange
+ var manifestContent = @"
+
+
+";
+
+ var manifest = CreateManifest(manifestContent);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => StaticWebAssetsReader.Parse(manifest).ToArray());
+ Assert.StartsWith("Invalid manifest", exception.Message);
+ }
+
+ [Fact]
+ public void ParseManifest_ParsesManifest_WithSingleItem()
+ {
+ // Arrange
+ var manifestContent = @"
+
+";
+
+ var manifest = CreateManifest(manifestContent);
+
+ // Act
+ var mappings = StaticWebAssetsReader.Parse(manifest).ToArray();
+
+ // Assert
+ var mapping = Assert.Single(mappings);
+ Assert.Equal("/Path", mapping.Path);
+ Assert.Equal("/BasePath", mapping.BasePath);
+ }
+
+ private Stream CreateManifest(string manifestContent)
+ {
+ return new MemoryStream(Encoding.UTF8.GetBytes(manifestContent));
+ }
+ }
+}