diff --git a/.csharpierrc b/.csharpierrc
new file mode 100644
index 0000000..7dd4c1f
--- /dev/null
+++ b/.csharpierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 100,
+ "useTabs": false,
+ "tabWidth": 4
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7d3fa4f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 80
+
+[*.cs]
+indent_size = 4
+max_line_length = 100
diff --git a/.github/workflows/continuous.yaml b/.github/workflows/continuous.yaml
new file mode 100644
index 0000000..0b461bf
--- /dev/null
+++ b/.github/workflows/continuous.yaml
@@ -0,0 +1,41 @@
+name: Continuous tests/builds/prerelease
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: "8.x"
+
+ - name: Pack ViteFest
+ working-directory: src/ViteFest
+ run: dotnet pack -c Release -o ../../dist
+
+ - name: Pack ViteFest.AspNetCore
+ working-directory: src/ViteFest.AspNetCore
+ run: dotnet pack -c Release -o ../../dist
+
+ - name: Push Nuget packages
+ working-directory: dist
+ env:
+ NUGET_API: https://api.nuget.org/v3/index.json
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ run: dotnet nuget push ./*.nupkg -s $NUGET_API -k $NUGET_API_KEY
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: dist
+ path: dist/**/*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8267c93
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+bin/
+obj/
+dist/
+
+.vs/
+.idea/
+*.user
+
+.DS_Store
+Thumbs.db
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3049222
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,24 @@
+{
+ "files.exclude": {
+ "**/.git": true,
+ "**/.idea": true,
+ "**/.vs": true,
+ "**/bin": true,
+ "**/obj": true,
+ "**/*.user": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true
+ },
+ "editor.rulers": [
+ 80,
+ 100
+ ],
+ "[csharp]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "csharpier.csharpier-vscode",
+ },
+ "[json][jsonc]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "vscode.json-language-features"
+ }
+}
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..1d9ea43
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+ enable
+ true
+ $(NoWarn);CS0436;CS1591;NETSDK1138;
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1956d9f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Robert Macfie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index e147e05..c51b24a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,40 @@
-# vitefest
\ No newline at end of file
+# ViteFest
+
+Utilize Vite build assets in ASP.NET Core and other .NET applications.
+
+
+## ASP.NET Core
+
+`vite.config.ts`:
+```typescript
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ base: '/dist/',
+ build: {
+ outDir: 'wwwroot/dist',
+ manifest: 'vite-manifest.json',
+ rollupOptions: {
+ input: ['ClientApp/shared.ts', 'ClientApp/login.ts']
+ }
+ }
+});
+```
+
+`Program.cs`:
+```csharp
+builder.Services.AddViteFest(o =>
+{
+ o.ManifestFile = "dist/vite-manifest.json";
+ o.BaseUrl = "/dist/";
+});
+```
+
+
+`Login.cshtml`:
+```html
+@inject ViteFest.IVite Vite
+
+
+
+```
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..8743ce6
--- /dev/null
+++ b/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "csharpier": {
+ "version": "0.28.2",
+ "commands": [
+ "dotnet-csharpier"
+ ]
+ }
+ }
+}
diff --git a/src/ViteFest.AspNetCore/Assembly.cs b/src/ViteFest.AspNetCore/Assembly.cs
new file mode 100644
index 0000000..fe9447b
--- /dev/null
+++ b/src/ViteFest.AspNetCore/Assembly.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("ViteFest.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/ViteFest.AspNetCore/ViteFest.AspNetCore.csproj b/src/ViteFest.AspNetCore/ViteFest.AspNetCore.csproj
new file mode 100644
index 0000000..fdd59cf
--- /dev/null
+++ b/src/ViteFest.AspNetCore/ViteFest.AspNetCore.csproj
@@ -0,0 +1,38 @@
+
+
+
+ net6.0
+ 12
+
+ true
+ snupkg
+ true
+
+ Robert Macfie
+ Copyright Robert Macfie
+ MIT
+ A simple extension to encapsulate DI configuration per .NET
+ project
+ vite
+ https://github.com/rmacfie/vitefest
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ViteFest.AspNetCore/ViteHostedService.cs b/src/ViteFest.AspNetCore/ViteHostedService.cs
new file mode 100644
index 0000000..0f20698
--- /dev/null
+++ b/src/ViteFest.AspNetCore/ViteHostedService.cs
@@ -0,0 +1,14 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting;
+
+namespace ViteFest.AspNetCore;
+
+internal class ViteHostedService(IViteState state) : BackgroundService
+{
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ state.Initialize();
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/ViteFest.AspNetCore/ViteServiceExtensions.cs b/src/ViteFest.AspNetCore/ViteServiceExtensions.cs
new file mode 100644
index 0000000..899b143
--- /dev/null
+++ b/src/ViteFest.AspNetCore/ViteServiceExtensions.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using ViteFest;
+using ViteFest.AspNetCore;
+
+#pragma warning disable IDE0130 // ReSharper disable CheckNamespace
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class ViteServiceExtensions
+{
+ public static IServiceCollection AddViteFest(
+ this IServiceCollection services,
+ Action configure
+ )
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ if (services.Any(x => x.ServiceType == typeof(Vite)))
+ {
+ throw new InvalidOperationException(
+ "ViteFest has already been added to the service collection."
+ );
+ }
+
+ var options = new ViteOptions();
+ configure(options);
+ options.Validate();
+
+ services.AddSingleton(x =>
+ {
+ var hostEnvironment = x.GetRequiredService();
+ return new ViteEnvironment(
+ options,
+ hostEnvironment.WebRootPath,
+ hostEnvironment.IsDevelopment()
+ );
+ });
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddHostedService();
+
+ return services;
+ }
+}
diff --git a/src/ViteFest.Tests/Stub.cs b/src/ViteFest.Tests/Stub.cs
new file mode 100644
index 0000000..c4898e7
--- /dev/null
+++ b/src/ViteFest.Tests/Stub.cs
@@ -0,0 +1,27 @@
+namespace ViteFest.Tests;
+
+internal static class Stub
+{
+ internal static ViteResource Resource(
+ string key,
+ string? path = null,
+ bool isEntry = false,
+ bool isDynamicEntry = false,
+ string[]? imports = null,
+ string[]? dynamicImports = null,
+ string[]? assetPaths = null,
+ string[]? cssPaths = null
+ )
+ {
+ return new ViteResource(
+ key,
+ path ?? "dir/" + key,
+ isEntry,
+ isDynamicEntry,
+ imports ?? [],
+ dynamicImports ?? [],
+ assetPaths ?? [],
+ cssPaths ?? []
+ );
+ }
+}
diff --git a/src/ViteFest.Tests/TestFiles/manifest-empty.json b/src/ViteFest.Tests/TestFiles/manifest-empty.json
new file mode 100644
index 0000000..e69de29
diff --git a/src/ViteFest.Tests/TestFiles/manifest-invalid.json b/src/ViteFest.Tests/TestFiles/manifest-invalid.json
new file mode 100644
index 0000000..3776ed5
--- /dev/null
+++ b/src/ViteFest.Tests/TestFiles/manifest-invalid.json
@@ -0,0 +1 @@
+This is not valid JSON syntax.
diff --git a/src/ViteFest.Tests/TestFiles/manifest.json b/src/ViteFest.Tests/TestFiles/manifest.json
new file mode 100644
index 0000000..729e76b
--- /dev/null
+++ b/src/ViteFest.Tests/TestFiles/manifest.json
@@ -0,0 +1,27 @@
+{
+ "../../node_modules/@fontsource-variable/inter/files/inter-latin-slnt-normal.woff2": {
+ "file": "assets/inter-latin-slnt-normal-BpMivaJw.woff2",
+ "src": "../../node_modules/@fontsource-variable/inter/files/inter-latin-slnt-normal.woff2"
+ },
+ "Components/Common/Logotype.svg": {
+ "file": "assets/Logotype-BQihiMnY.svg",
+ "src": "Components/Common/Logotype.svg"
+ },
+ "Components/Home.ts": {
+ "file": "assets/Home-B2lgNECc.js",
+ "name": "Home",
+ "src": "Components/Home.ts",
+ "isEntry": true,
+ "css": [
+ "assets/Home-Bs7ZzuBZ.css"
+ ],
+ "assets": [
+ "assets/Logotype-BQihiMnY.svg"
+ ]
+ },
+ "Components/Root.css": {
+ "file": "assets/Root-B-NYCzS9.css",
+ "src": "Components/Root.css",
+ "isEntry": true
+ }
+}
diff --git a/src/ViteFest.Tests/ViteEnvironmentTests.cs b/src/ViteFest.Tests/ViteEnvironmentTests.cs
new file mode 100644
index 0000000..1688845
--- /dev/null
+++ b/src/ViteFest.Tests/ViteEnvironmentTests.cs
@@ -0,0 +1,76 @@
+using System.IO;
+using NUnit.Framework;
+
+namespace ViteFest.Tests;
+
+public class ViteEnvironmentTests
+{
+ [Test]
+ public void It_uses_complete_values_from_options()
+ {
+ var env = new ViteEnvironment(
+ new ViteOptions
+ {
+ ManifestFile = "/dev/foo/wwwroot/dist/manifest.json",
+ BaseUrl = "/dist/",
+ Watch = true
+ }
+ );
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(env.ManifestFile, Is.EqualTo("/dev/foo/wwwroot/dist/manifest.json"));
+ Assert.That(env.BaseUrl, Is.EqualTo("/dist/"));
+ Assert.That(env.Watch, Is.True);
+ });
+ }
+
+ [Test]
+ public void It_combines_web_root_with_relative_manifest_path()
+ {
+ var env = new ViteEnvironment(
+ new ViteOptions { ManifestFile = "dist/manifest.json" },
+ "/dev/foo/wwwroot"
+ );
+
+ Assert.That(env.ManifestFile, Is.EqualTo("/dev/foo/wwwroot/dist/manifest.json"));
+ }
+
+ [Test]
+ public void It_defaults_to_root_base_url()
+ {
+ var env = new ViteEnvironment(new ViteOptions { ManifestFile = "/dev/proj/manifest.json" });
+
+ Assert.That(env.BaseUrl, Is.EqualTo("/"));
+ }
+
+ [Test]
+ public void It_defaults_to_disable_watch()
+ {
+ var env = new ViteEnvironment(new ViteOptions { ManifestFile = "/dev/proj/manifest.json" });
+
+ Assert.That(env.Watch, Is.False);
+ }
+
+ [Test]
+ public void It_falls_back_on_dev_mode_for_watch()
+ {
+ var env = new ViteEnvironment(
+ new ViteOptions { ManifestFile = "/dev/proj/manifest.json" },
+ isDevelopment: true
+ );
+
+ Assert.That(env.Watch, Is.True);
+ }
+
+ [Test]
+ public void It_falls_back_on_current_dir_for_web_root()
+ {
+ var env = new ViteEnvironment(new ViteOptions { ManifestFile = "dist/manifest.json" });
+
+ Assert.That(
+ env.ManifestFile,
+ Is.EqualTo(Path.Combine(Directory.GetCurrentDirectory(), "dist/manifest.json"))
+ );
+ }
+}
diff --git a/src/ViteFest.Tests/ViteFest.Tests.csproj b/src/ViteFest.Tests/ViteFest.Tests.csproj
new file mode 100644
index 0000000..145b20b
--- /dev/null
+++ b/src/ViteFest.Tests/ViteFest.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/ViteFest.Tests/ViteManifestReaderTests.cs b/src/ViteFest.Tests/ViteManifestReaderTests.cs
new file mode 100644
index 0000000..c89d453
--- /dev/null
+++ b/src/ViteFest.Tests/ViteManifestReaderTests.cs
@@ -0,0 +1,129 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using NUnit.Framework;
+
+namespace ViteFest.Tests;
+
+public class ViteManifestReaderTests
+{
+ private const string ManifestFile = "TestFiles/manifest.json";
+ private const string ManifestEmptyFile = "TestFiles/manifest-empty.json";
+ private const string ManifestInvalidFile = "TestFiles/manifest-invalid.json";
+ private const string ManifestNonExistentFile = "TestFiles/manifest-not-exists.json";
+
+ private ViteManifestReader _sut;
+
+ [SetUp]
+ public void Setup()
+ {
+ _sut = new ViteManifestReader();
+ }
+
+ [Test]
+ public void It_reads_a_manifest_file()
+ {
+ // act
+ var manifest = _sut.ReadManifest(ManifestFile);
+
+ // assert
+ Assert.That(manifest, Is.Not.Null);
+ }
+
+ [Test]
+ public void It_maps_an_entry_script_chunk()
+ {
+ var manifest = _sut.ReadManifest(ManifestFile);
+
+ // act
+ var chunk = manifest.FirstOrDefault(x => x.Src == "Components/Home.ts")!;
+
+ // assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(chunk, Is.Not.Null);
+ Assert.That(chunk.File, Is.EqualTo("assets/Home-B2lgNECc.js"));
+ Assert.That(chunk.IsEntry, Is.True);
+ Assert.That(chunk.IsDynamicEntry, Is.Null);
+ Assert.That(chunk.Imports, Is.Null);
+ Assert.That(chunk.DynamicImports, Is.Null);
+ Assert.That(chunk.Assets, Is.EquivalentTo(new[] { "assets/Logotype-BQihiMnY.svg" }));
+ Assert.That(chunk.Css, Is.EquivalentTo(new[] { "assets/Home-Bs7ZzuBZ.css" }));
+ });
+ }
+
+ [Test]
+ public void It_maps_an_entry_css_chunk()
+ {
+ var manifest = _sut.ReadManifest(ManifestFile);
+
+ // act
+ var chunk = manifest.FirstOrDefault(x => x.Src == "Components/Root.css")!;
+
+ // assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(chunk, Is.Not.Null);
+ Assert.That(chunk.File, Is.EqualTo("assets/Root-B-NYCzS9.css"));
+ Assert.That(chunk.IsEntry, Is.True);
+ Assert.That(chunk.IsDynamicEntry, Is.Null);
+ Assert.That(chunk.Imports, Is.Null);
+ Assert.That(chunk.DynamicImports, Is.Null);
+ Assert.That(chunk.Assets, Is.Null);
+ Assert.That(chunk.Css, Is.Null);
+ });
+ }
+
+ [Test]
+ public void It_maps_an_asset_chunk()
+ {
+ var manifest = _sut.ReadManifest(ManifestFile);
+
+ // act
+ var chunk = manifest.FirstOrDefault(x => x.Src == "Components/Common/Logotype.svg")!;
+
+ // assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(chunk, Is.Not.Null);
+ Assert.That(chunk.File, Is.EqualTo("assets/Logotype-BQihiMnY.svg"));
+ Assert.That(chunk.IsEntry, Is.Null);
+ Assert.That(chunk.IsDynamicEntry, Is.Null);
+ Assert.That(chunk.Imports, Is.Null);
+ Assert.That(chunk.DynamicImports, Is.Null);
+ Assert.That(chunk.Assets, Is.Null);
+ Assert.That(chunk.Css, Is.Null);
+ });
+ }
+
+ [Test]
+ public void It_throws_when_manifest_file_is_empty()
+ {
+ // act
+ var act = new Action(() => _sut.ReadManifest(ManifestEmptyFile));
+
+ // assert
+ Assert.That(act, Throws.TypeOf());
+ }
+
+ [Test]
+ public void It_throws_when_manifest_file_is_invalid()
+ {
+ // act
+ var act = new Action(() => _sut.ReadManifest(ManifestInvalidFile));
+
+ // assert
+ Assert.That(act, Throws.TypeOf());
+ }
+
+ [Test]
+ public void It_throws_when_manifest_file_is_missing()
+ {
+ // act
+ var act = new Action(() => _sut.ReadManifest(ManifestNonExistentFile));
+
+ // assert
+ Assert.That(act, Throws.TypeOf());
+ }
+}
diff --git a/src/ViteFest.Tests/ViteResourceMapperTests.cs b/src/ViteFest.Tests/ViteResourceMapperTests.cs
new file mode 100644
index 0000000..e7b690a
--- /dev/null
+++ b/src/ViteFest.Tests/ViteResourceMapperTests.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using System.Linq;
+using FakeItEasy;
+using NUnit.Framework;
+
+namespace ViteFest.Tests;
+
+public class ViteResourceMapperTests
+{
+ private IViteEnvironment _environment;
+ private ViteResourceMapper _sut;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _environment = A.Fake();
+ A.CallTo(() => _environment.BaseUrl).Returns("/dist/");
+
+ _sut = new ViteResourceMapper(_environment);
+ }
+
+ [Test]
+ public void It_maps_all_values()
+ {
+ var resources = _sut.Map(
+ new[]
+ {
+ new ViteManifestChunk
+ {
+ Src = "a.ts",
+ File = "assets/a.js",
+ IsEntry = true,
+ IsDynamicEntry = true,
+ Imports = new List { "b.ts" },
+ DynamicImports = new List { "c.ts" },
+ Css = new List { "assets/styles.css" },
+ Assets = new List { "assets/image.png" }
+ }
+ }
+ );
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(resources, Has.Count.EqualTo(1));
+
+ var resource = resources.Single();
+
+ Assert.That(resource.Key, Is.EqualTo("a.ts"));
+ Assert.That(resource.Url, Is.EqualTo("/dist/assets/a.js"));
+ Assert.That(resource.IsEntry, Is.True);
+ Assert.That(resource.IsDynamicEntry, Is.True);
+ Assert.That(resource.Imports, Is.EquivalentTo(new[] { "b.ts" }));
+ Assert.That(resource.DynamicImports, Is.EquivalentTo(new[] { "c.ts" }));
+ Assert.That(resource.CssUrls, Is.EquivalentTo(new[] { "/dist/assets/styles.css" }));
+ Assert.That(resource.AssetUrls, Is.EquivalentTo(new[] { "/dist/assets/image.png" }));
+ });
+ }
+
+ [Test]
+ public void It_maps_empty_optional_values()
+ {
+ var resources = _sut.Map(
+ new[]
+ {
+ new ViteManifestChunk { Src = "a.ts", File = "assets/a.js" }
+ }
+ );
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(resources, Has.Count.EqualTo(1));
+
+ var resource = resources.Single();
+
+ Assert.That(resource.Key, Is.EqualTo("a.ts"));
+ Assert.That(resource.Url, Is.EqualTo("/dist/assets/a.js"));
+ Assert.That(resource.IsEntry, Is.False);
+ Assert.That(resource.IsDynamicEntry, Is.False);
+ Assert.That(resource.Imports, Is.Empty);
+ Assert.That(resource.DynamicImports, Is.Empty);
+ Assert.That(resource.CssUrls, Is.Empty);
+ Assert.That(resource.AssetUrls, Is.Empty);
+ });
+ }
+}
diff --git a/src/ViteFest.Tests/ViteStateTests.cs b/src/ViteFest.Tests/ViteStateTests.cs
new file mode 100644
index 0000000..347e2a9
--- /dev/null
+++ b/src/ViteFest.Tests/ViteStateTests.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using FakeItEasy;
+using NUnit.Framework;
+
+// ReSharper disable CollectionNeverUpdated.Local
+
+namespace ViteFest.Tests;
+
+public class ViteStateTests
+{
+ private List _resources;
+ private ViteState _sut;
+
+ [TearDown]
+ public void TearDown()
+ {
+ _sut.Dispose();
+ }
+
+ [SetUp]
+ public void Setup()
+ {
+ var chunks = new List();
+ _resources = [];
+
+ var environment = A.Fake();
+ var manifestReader = A.Fake();
+ var resourceMapper = A.Fake();
+
+ A.CallTo(() => environment.ManifestFile).Returns("/tmp/manifest.json");
+ A.CallTo(() => manifestReader.ReadManifest("/tmp/manifest.json")).Returns(chunks);
+ A.CallTo(() => resourceMapper.Map(chunks)).Returns(_resources);
+
+ _sut = new ViteState(environment, manifestReader, resourceMapper);
+ }
+
+ [Test]
+ public void TryGet_finds_existing_chunk()
+ {
+ var chunkA = Stub.Resource("a.ts");
+ var chunkB = Stub.Resource("b.ts");
+ _resources.AddRange(new[] { chunkA, chunkB });
+ _sut.Initialize();
+
+ var actualReturn = _sut.TryGet("b.ts", out var actualChunk);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualReturn, Is.True);
+ Assert.That(actualChunk, Is.SameAs(chunkB));
+ });
+ }
+
+ [Test]
+ public void TryGet_is_case_insensitive()
+ {
+ var chunk = Stub.Resource("CHUNKNaME.ts");
+ _resources.AddRange(new[] { chunk });
+ _sut.Initialize();
+
+ var actualReturn = _sut.TryGet("chunknAme.ts", out var actualChunk);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualReturn, Is.True);
+ Assert.That(actualChunk, Is.SameAs(chunk));
+ });
+ }
+
+ [Test]
+ public void TryGet_returns_null_when_chunk_is_not_loaded()
+ {
+ var chunk = Stub.Resource("a.ts");
+ _resources.AddRange(new[] { chunk });
+ _sut.Initialize();
+
+ var actualReturn = _sut.TryGet("b.ts", out var actualChunk);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualReturn, Is.False);
+ Assert.That(actualChunk, Is.Null);
+ });
+ }
+
+ [Test]
+ public void Initialize_throws_when_called_twice()
+ {
+ _sut.Initialize();
+
+ var act = new Action(() => _sut.Initialize());
+
+ Assert.That(act, Throws.InvalidOperationException);
+ }
+
+ [Test]
+ public void TryGet_throws_if_not_initialized()
+ {
+ var act = new Action(() => _sut.TryGet("a.ts", out _));
+
+ Assert.That(act, Throws.InvalidOperationException);
+ }
+}
diff --git a/src/ViteFest/Assembly.cs b/src/ViteFest/Assembly.cs
new file mode 100644
index 0000000..6447c90
--- /dev/null
+++ b/src/ViteFest/Assembly.cs
@@ -0,0 +1,5 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("ViteFest.AspNetCore")]
+[assembly: InternalsVisibleTo("ViteFest.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/ViteFest/IVite.cs b/src/ViteFest/IVite.cs
new file mode 100644
index 0000000..29b16bb
--- /dev/null
+++ b/src/ViteFest/IVite.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace ViteFest;
+
+public interface IVite : IDisposable
+{
+ string? GetUrl(string key);
+
+ IReadOnlyCollection GetImports(string key);
+
+ IReadOnlyCollection GetDynamicImports(string key);
+
+ IReadOnlyCollection GetAssetUrls(string key);
+
+ IReadOnlyCollection GetCssUrls(string key);
+
+ bool TryGetUrl(string key, [NotNullWhen(true)] out string? path);
+
+ bool TryGetImports(string key, [NotNullWhen(true)] out IReadOnlyCollection? imports);
+
+ bool TryGetDynamicImports(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? dynamicImports
+ );
+
+ bool TryGetAssetUrls(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? assetPaths
+ );
+
+ bool TryGetCssUrls(string key, [NotNullWhen(true)] out IReadOnlyCollection? cssPaths);
+}
+
+public sealed class Vite : IVite
+{
+ private readonly IViteState _state;
+
+ internal Vite(IViteState state)
+ {
+ _state = state;
+ }
+
+ public string? GetUrl(string key)
+ {
+ return TryGetUrl(key, out var path) ? path : default;
+ }
+
+ public IReadOnlyCollection GetImports(string key)
+ {
+ return TryGetImports(key, out var imports) ? imports : Array.Empty();
+ }
+
+ public IReadOnlyCollection GetDynamicImports(string key)
+ {
+ return TryGetDynamicImports(key, out var imports) ? imports : Array.Empty();
+ }
+
+ public IReadOnlyCollection GetAssetUrls(string key)
+ {
+ return TryGetAssetUrls(key, out var paths) ? paths : Array.Empty();
+ }
+
+ public IReadOnlyCollection GetCssUrls(string key)
+ {
+ return TryGetCssUrls(key, out var paths) ? paths : Array.Empty();
+ }
+
+ public bool TryGetUrl(string key, [NotNullWhen(true)] out string? path)
+ {
+ if (!_state.TryGet(key, out var chunk))
+ {
+ path = default;
+ return false;
+ }
+
+ path = chunk.Url;
+ return true;
+ }
+
+ public bool TryGetImports(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? imports
+ )
+ {
+ if (!_state.TryGet(key, out var chunk))
+ {
+ imports = default;
+ return false;
+ }
+
+ imports = chunk.Imports;
+ return true;
+ }
+
+ public bool TryGetDynamicImports(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? dynamicImports
+ )
+ {
+ if (!_state.TryGet(key, out var chunk))
+ {
+ dynamicImports = default;
+ return false;
+ }
+
+ dynamicImports = chunk.DynamicImports;
+ return true;
+ }
+
+ public bool TryGetAssetUrls(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? assetPaths
+ )
+ {
+ if (!_state.TryGet(key, out var chunk))
+ {
+ assetPaths = default;
+ return false;
+ }
+
+ assetPaths = chunk.AssetUrls;
+ return true;
+ }
+
+ public bool TryGetCssUrls(
+ string key,
+ [NotNullWhen(true)] out IReadOnlyCollection? cssPaths
+ )
+ {
+ if (!_state.TryGet(key, out var chunk))
+ {
+ cssPaths = default;
+ return false;
+ }
+
+ cssPaths = chunk.CssUrls;
+ return true;
+ }
+
+ public void Dispose()
+ {
+ _state.Dispose();
+ }
+
+ public static IVite Create(Action configure)
+ {
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ var options = new ViteOptions();
+ configure(options);
+ return Create(options);
+ }
+
+ public static IVite Create(ViteOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ options.Validate();
+
+ var env = new ViteEnvironment(options);
+ var manifestReader = new ViteManifestReader();
+ var resourceMapper = new ViteResourceMapper(env);
+ var state = new ViteState(env, manifestReader, resourceMapper);
+
+ state.Initialize();
+
+ return new Vite(state);
+ }
+}
diff --git a/src/ViteFest/IViteEnvironment.cs b/src/ViteFest/IViteEnvironment.cs
new file mode 100644
index 0000000..e6699df
--- /dev/null
+++ b/src/ViteFest/IViteEnvironment.cs
@@ -0,0 +1,42 @@
+using System;
+using System.IO;
+
+namespace ViteFest
+{
+ public interface IViteEnvironment
+ {
+ string ManifestFile { get; }
+
+ string BaseUrl { get; }
+
+ bool Watch { get; }
+ }
+
+ public class ViteEnvironment : IViteEnvironment
+ {
+ public ViteEnvironment(
+ ViteOptions options,
+ string? webRootPath = null,
+ bool isDevelopment = false
+ )
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ ManifestFile = Path.IsPathRooted(options.ManifestFile)
+ ? options.ManifestFile
+ : Path.Combine(
+ webRootPath ?? Directory.GetCurrentDirectory(),
+ options.ManifestFile
+ );
+ BaseUrl = options.BaseUrl ?? "/";
+ Watch = options.Watch ?? isDevelopment;
+ }
+
+ public string ManifestFile { get; }
+ public string BaseUrl { get; }
+ public bool Watch { get; }
+ }
+}
diff --git a/src/ViteFest/IViteManifestReader.cs b/src/ViteFest/IViteManifestReader.cs
new file mode 100644
index 0000000..fe7552f
--- /dev/null
+++ b/src/ViteFest/IViteManifestReader.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+
+namespace ViteFest
+{
+ internal interface IViteManifestReader
+ {
+ IReadOnlyCollection ReadManifest(string manifestPath);
+ }
+
+ internal class ViteManifestReader : IViteManifestReader
+ {
+ private static JsonSerializerOptions JsonOptions { get; } = new();
+
+ public IReadOnlyCollection ReadManifest(string path)
+ {
+ var absolutePath = Path.GetFullPath(path);
+ var json = File.ReadAllText(absolutePath);
+ var chunks = JsonSerializer.Deserialize>(
+ json,
+ JsonOptions
+ );
+
+ if (chunks is null)
+ {
+ throw new Exception($"The manifest file was empty ('{absolutePath}')");
+ }
+
+ return chunks.Values;
+ }
+ }
+}
diff --git a/src/ViteFest/IViteResourceMapper.cs b/src/ViteFest/IViteResourceMapper.cs
new file mode 100644
index 0000000..307c6c8
--- /dev/null
+++ b/src/ViteFest/IViteResourceMapper.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ViteFest
+{
+ internal interface IViteResourceMapper
+ {
+ IReadOnlyCollection Map(IEnumerable chunks);
+ }
+
+ internal class ViteResourceMapper : IViteResourceMapper
+ {
+ private readonly IViteEnvironment _env;
+
+ public ViteResourceMapper(IViteEnvironment env)
+ {
+ _env = env;
+ }
+
+ public IReadOnlyCollection Map(IEnumerable chunks)
+ {
+ return chunks.Select(x => Map(x, _env.BaseUrl)).ToArray();
+ }
+
+ private static ViteResource Map(ViteManifestChunk chunk, string baseUrl)
+ {
+ return new ViteResource(
+ chunk.Src,
+ $"{baseUrl}{chunk.File}",
+ chunk.IsEntry ?? false,
+ chunk.IsDynamicEntry ?? false,
+ chunk.Imports?.ToArray() ?? Array.Empty(),
+ chunk.DynamicImports?.ToArray() ?? Array.Empty(),
+ chunk.Assets?.Select(path => $"{baseUrl}{path}").ToArray() ?? Array.Empty(),
+ chunk.Css?.Select(path => $"{baseUrl}{path}").ToArray() ?? Array.Empty()
+ );
+ }
+ }
+}
diff --git a/src/ViteFest/IViteState.cs b/src/ViteFest/IViteState.cs
new file mode 100644
index 0000000..9991bdb
--- /dev/null
+++ b/src/ViteFest/IViteState.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+namespace ViteFest;
+
+internal interface IViteState : IDisposable
+{
+ void Initialize();
+ bool TryGet(string key, [NotNullWhen(true)] out ViteResource? resource);
+}
+
+internal sealed class ViteState : IViteState
+{
+ private readonly IViteEnvironment _environment;
+ private readonly IViteManifestReader _manifestReader;
+ private readonly IViteResourceMapper _resourceMapper;
+
+ private Timer? _debouncer;
+ private Dictionary? _state;
+ private FileSystemWatcher? _watcher;
+
+ public ViteState(
+ IViteEnvironment environment,
+ IViteManifestReader manifestReader,
+ IViteResourceMapper resourceMapper
+ )
+ {
+ _environment = environment;
+ _manifestReader = manifestReader;
+ _resourceMapper = resourceMapper;
+ }
+
+ public void Initialize()
+ {
+ if (_state != null)
+ {
+ throw new InvalidOperationException(
+ "The registry can't be initialized more than once."
+ );
+ }
+
+ Load();
+
+ if (_environment.Watch)
+ {
+ _debouncer = new Timer(_ => Load());
+ _watcher = new FileSystemWatcher(
+ Path.GetDirectoryName(_environment.ManifestFile)!,
+ Path.GetFileName(_environment.ManifestFile)
+ )
+ {
+ NotifyFilter = NotifyFilters.LastWrite
+ };
+
+ _watcher.Changed += (_, _) =>
+ _debouncer.Change(TimeSpan.FromMilliseconds(200), Timeout.InfiniteTimeSpan);
+
+ _watcher.EnableRaisingEvents = true;
+ }
+ }
+
+ public bool TryGet(string key, [NotNullWhen(true)] out ViteResource? resource)
+ {
+ if (_state == null)
+ {
+ throw new InvalidOperationException("The registry must be initialized before use.");
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var normalizedKey = key.TrimStart('/');
+ return _state.TryGetValue(normalizedKey, out resource);
+ }
+
+ public void Dispose()
+ {
+ _watcher?.Dispose();
+ _debouncer?.Dispose();
+ }
+
+ private void Load()
+ {
+ var chunks = _manifestReader.ReadManifest(_environment.ManifestFile);
+ var resources = _resourceMapper.Map(chunks);
+ _state = resources.ToDictionary(x => x.Key, x => x, StringComparer.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/ViteFest/ViteFest.csproj b/src/ViteFest/ViteFest.csproj
new file mode 100644
index 0000000..8dab94b
--- /dev/null
+++ b/src/ViteFest/ViteFest.csproj
@@ -0,0 +1,33 @@
+
+
+ netstandard2.0
+ 10
+
+ true
+ snupkg
+ true
+
+ Robert Macfie
+ Copyright Robert Macfie
+ MIT
+ A simple extension to encapsulate DI configuration per .NET
+ project
+ vite
+ https://github.com/rmacfie/vitefest
+ README.md
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/src/ViteFest/ViteManifestChunk.cs b/src/ViteFest/ViteManifestChunk.cs
new file mode 100644
index 0000000..81ed253
--- /dev/null
+++ b/src/ViteFest/ViteManifestChunk.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace ViteFest;
+
+///
+/// Represents a raw chunk from the Vite manifest file.
+/// See https://vitejs.dev/guide/backend-integration
+///
+internal class ViteManifestChunk
+{
+ [JsonPropertyName("src")]
+ public string Src { get; set; } = default!;
+
+ [JsonPropertyName("file")]
+ public string File { get; set; } = default!;
+
+ [JsonPropertyName("isEntry")]
+ public bool? IsEntry { get; set; }
+
+ [JsonPropertyName("isDynamicEntry")]
+ public bool? IsDynamicEntry { get; set; }
+
+ [JsonPropertyName("imports")]
+ public List? Imports { get; set; }
+
+ [JsonPropertyName("dynamicImports")]
+ public List? DynamicImports { get; set; }
+
+ [JsonPropertyName("assets")]
+ public List? Assets { get; set; }
+
+ [JsonPropertyName("css")]
+ public List? Css { get; set; }
+}
diff --git a/src/ViteFest/ViteOptions.cs b/src/ViteFest/ViteOptions.cs
new file mode 100644
index 0000000..1219e8c
--- /dev/null
+++ b/src/ViteFest/ViteOptions.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace ViteFest
+{
+ public class ViteOptions
+ {
+ ///
+ /// The file system path of the manifest file.
+ /// When a relative path is provided, it is resolved relative to the application's
+ /// content root path.
+ ///
+ public string ManifestFile { get; set; } = default!;
+
+ ///
+ /// The base public path, or base URL, to the Vite output directory.
+ /// Defaults to "/".
+ ///
+ public string? BaseUrl { get; set; }
+
+ ///
+ /// Indicates whether to watch and reload the manifest file when it changes.
+ /// Defaults to false in Production environments and true in Development.
+ ///
+ public bool? Watch { get; set; }
+
+ public void Validate()
+ {
+ if (string.IsNullOrEmpty(ManifestFile))
+ {
+ throw new Exception($"The {nameof(ManifestFile)} option is required");
+ }
+ }
+ }
+}
diff --git a/src/ViteFest/ViteResource.cs b/src/ViteFest/ViteResource.cs
new file mode 100644
index 0000000..27ba599
--- /dev/null
+++ b/src/ViteFest/ViteResource.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+
+namespace ViteFest
+{
+ internal sealed class ViteResource
+ {
+ public ViteResource(
+ string key,
+ string url,
+ bool isEntry,
+ bool isDynamicEntry,
+ IReadOnlyCollection imports,
+ IReadOnlyCollection dynamicImports,
+ IReadOnlyCollection assetUrls,
+ IReadOnlyCollection cssUrls
+ )
+ {
+ Key = key;
+ Url = url;
+ IsEntry = isEntry;
+ IsDynamicEntry = isDynamicEntry;
+ Imports = imports;
+ DynamicImports = dynamicImports;
+ AssetUrls = assetUrls;
+ CssUrls = cssUrls;
+ }
+
+ ///
+ /// The key of the chunk, which usually is the path of the source file,
+ /// relative to the project root.
+ ///
+ ///
+ /// "Client/main.ts"
+ ///
+ public string Key { get; }
+
+ ///
+ /// The public absolute path to the chunk, for use in the client.
+ ///
+ ///
+ /// This corresponds to the file property in the Vite manifest.
+ ///
+ ///
+ /// "/dist/assets/main.oauo411t.js"
+ ///
+ public string Url { get; }
+
+ public bool IsEntry { get; }
+
+ public bool IsDynamicEntry { get; }
+
+ ///
+ /// Scripts imported by this source. The values are the keys of the chunks
+ /// that details the imported scripts.
+ ///
+ ///
+ /// ["Components/Foo.ts", "Components/Bar.ts"]
+ ///
+ public IReadOnlyCollection Imports { get; }
+
+ ///
+ /// Scripts dynamically imported by this source. The values are the keys of the chunks
+ /// that details the imported scripts.
+ ///
+ ///
+ /// ["Components/Foo.ts", "Components/Bar.ts"]
+ ///
+ public IReadOnlyCollection DynamicImports { get; }
+
+ ///
+ /// Asset files imported by this source. The values are the public paths of the files,
+ /// relative to base, for use in the client.
+ /// This corresponds to the assets property in the Vite manifest.
+ ///
+ ///
+ /// ["/dist/assets/Photo1.nsha62mx.jpg", "/dist/assets/Logotype.9adf52l.svg"]
+ ///
+ public IReadOnlyCollection AssetUrls { get; }
+
+ ///
+ /// CSS files imported by this source. The values are the public paths of the files,
+ /// relative to base, for use in the client.
+ /// This corresponds to the css property in the Vite manifest.
+ ///
+ ///
+ /// ["/dist/assets/main.nsha62mx.css", "/dist/assets/main.9adf52l.css"]
+ ///
+ public IReadOnlyCollection CssUrls { get; }
+ }
+}
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..393dddd
--- /dev/null
+++ b/version.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "1.0-beta",
+ "publicReleaseRefSpec": [
+ "^refs/heads/main$",
+ "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ ],
+ "cloudBuild": {
+ "buildNumber": {
+ "enabled": true
+ }
+ }
+}
diff --git a/vitefest.sln b/vitefest.sln
new file mode 100644
index 0000000..1498c2e
--- /dev/null
+++ b/vitefest.sln
@@ -0,0 +1,57 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{21FE8480-F9F2-406C-9935-8DC9F3AEC5A4}"
+EndProject
+
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViteFest", "src\ViteFest\ViteFest.csproj", "{EB9E73CA-24F9-4AC4-AC28-ED33F7328481}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViteFest.AspNetCore", "src\ViteFest.AspNetCore\ViteFest.AspNetCore.csproj", "{27E24FD1-84D7-4D18-8036-F65A20A1A001}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViteFest.Tests", "src\ViteFest.Tests\ViteFest.Tests.csproj", "{21B96868-6B6F-44E0-BBFE-058F1C430C1B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{CFF044DD-C879-4F71-830E-356AAF662F9F}"
+ ProjectSection(SolutionItems) = preProject
+ .csharpierrc = .csharpierrc
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ Directory.Build.props = Directory.Build.props
+ LICENSE = LICENSE
+ README.md = README.md
+ version.json = version.json
+ EndProjectSection
+EndProject
+
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EB9E73CA-24F9-4AC4-AC28-ED33F7328481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB9E73CA-24F9-4AC4-AC28-ED33F7328481}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB9E73CA-24F9-4AC4-AC28-ED33F7328481}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB9E73CA-24F9-4AC4-AC28-ED33F7328481}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27E24FD1-84D7-4D18-8036-F65A20A1A001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {27E24FD1-84D7-4D18-8036-F65A20A1A001}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {27E24FD1-84D7-4D18-8036-F65A20A1A001}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {27E24FD1-84D7-4D18-8036-F65A20A1A001}.Release|Any CPU.Build.0 = Release|Any CPU
+ {21B96868-6B6F-44E0-BBFE-058F1C430C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {21B96868-6B6F-44E0-BBFE-058F1C430C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {21B96868-6B6F-44E0-BBFE-058F1C430C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {21B96868-6B6F-44E0-BBFE-058F1C430C1B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {EB9E73CA-24F9-4AC4-AC28-ED33F7328481} = {21FE8480-F9F2-406C-9935-8DC9F3AEC5A4}
+ {27E24FD1-84D7-4D18-8036-F65A20A1A001} = {21FE8480-F9F2-406C-9935-8DC9F3AEC5A4}
+ {21B96868-6B6F-44E0-BBFE-058F1C430C1B} = {21FE8480-F9F2-406C-9935-8DC9F3AEC5A4}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E6514A26-A7DE-40E6-8373-791D63CCDC39}
+ EndGlobalSection
+EndGlobal