1 Commits

Author SHA1 Message Date
swcs 25f0c570f1 Update ViteProxyApplicationBuilderExtensions.cs 2024-05-24 13:34:34 +02:00
7 changed files with 105 additions and 150 deletions
+48 -48
View File
@@ -8,27 +8,27 @@ namespace ViteProxy;
public class ProcessProxy public class ProcessProxy
{ {
readonly string _workingDirectory; string workingDirectory;
readonly string _script; string script;
Action<ProcessStartInfo> _onProcessConfigure = null; Action<ProcessStartInfo> onProcessConfigure = null;
Action<string, bool> _captureLog = null; Action<string, bool> captureLog = null;
bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); bool isWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
readonly Dictionary<string, string> _envVars = new(); Dictionary<string, string> envVars = new();
readonly HashSet<string> _arguments = new(); HashSet<string> arguments = new();
readonly bool _forwardLog = false; bool forwardLog = false;
Process _process = null; Process process = null;
EventedStreamReader _stdOut; EventedStreamReader stdOut;
EventedStreamReader _stdErr; EventedStreamReader stdErr;
public ProcessProxy(string workingDirectory, string script, bool forwardLog = false) public ProcessProxy(string workingDirectory, string script, bool forwardLog = false)
{ {
this._workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
this._script = script; this.script = script;
this._forwardLog = forwardLog; this.forwardLog = forwardLog;
} }
@@ -37,7 +37,7 @@ public class ProcessProxy
/// </summary> /// </summary>
public ProcessProxy EnvVar(string key, string value) public ProcessProxy EnvVar(string key, string value)
{ {
_envVars.Add(key, value); envVars.Add(key, value);
return this; return this;
} }
@@ -47,7 +47,7 @@ public class ProcessProxy
/// </summary> /// </summary>
public ProcessProxy Argument(string argument) public ProcessProxy Argument(string argument)
{ {
_arguments.Add(argument); arguments.Add(argument);
return this; return this;
} }
@@ -57,7 +57,7 @@ public class ProcessProxy
/// </summary> /// </summary>
public ProcessProxy Configure(Action<ProcessStartInfo> onProcessConfigure) public ProcessProxy Configure(Action<ProcessStartInfo> onProcessConfigure)
{ {
this._onProcessConfigure = onProcessConfigure; this.onProcessConfigure = onProcessConfigure;
return this; return this;
} }
@@ -67,7 +67,7 @@ public class ProcessProxy
/// </summary> /// </summary>
public ProcessProxy Capture(Action<string, bool> action) public ProcessProxy Capture(Action<string, bool> action)
{ {
this._captureLog = action; this.captureLog = action;
return this; return this;
} }
@@ -80,16 +80,16 @@ public class ProcessProxy
{ {
StartProcess(); StartProcess();
using var stdErrReader = new EventedStreamStringReader(_stdErr); using var stdErrReader = new EventedStreamStringReader(stdErr);
try try
{ {
// TODO implement timeout // TODO implement timeout
// https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource // https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource
await _stdOut.WaitForFinish(); await stdOut.WaitForFinish();
} }
catch (EndOfStreamException ex) catch (EndOfStreamException ex)
{ {
throw new InvalidOperationException($"The script '{_script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
} }
} }
@@ -102,22 +102,22 @@ public class ProcessProxy
{ {
StartProcess(); StartProcess();
using var stdErrReader = new EventedStreamStringReader(_stdErr); using var stdErrReader = new EventedStreamStringReader(stdErr);
try try
{ {
await _stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout)); await stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout));
} }
catch (EndOfStreamException ex) catch (EndOfStreamException ex)
{ {
throw new InvalidOperationException($"The script '{_script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
} }
} }
public void Exit() public void Exit()
{ {
try { _process?.Kill(); } catch { } try { process?.Kill(); } catch { }
try { _process?.WaitForExit(); } catch { } try { process?.WaitForExit(); } catch { }
AppDomain.CurrentDomain.DomainUnload -= UnloadHandler; AppDomain.CurrentDomain.DomainUnload -= UnloadHandler;
AppDomain.CurrentDomain.ProcessExit -= UnloadHandler; AppDomain.CurrentDomain.ProcessExit -= UnloadHandler;
@@ -130,18 +130,18 @@ public class ProcessProxy
/// </summary> /// </summary>
Process StartProcess() Process StartProcess()
{ {
string executable = _script; string executable = script;
StringBuilder command = new(); StringBuilder command = new();
command.Append(_script); command.Append(script);
command.Append(' '); command.Append(' ');
foreach (string arg in _arguments) foreach (string arg in arguments)
{ {
command.Append(arg); command.Append(arg);
} }
string argumentList = command.ToString(); string argumentList = command.ToString();
if (IsWindows) if (isWindows)
{ {
argumentList = $"/c {argumentList}"; argumentList = $"/c {argumentList}";
executable = "cmd"; executable = "cmd";
@@ -154,20 +154,20 @@ public class ProcessProxy
RedirectStandardInput = true, RedirectStandardInput = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
WorkingDirectory = _workingDirectory WorkingDirectory = workingDirectory
}; };
foreach (var envVar in _envVars) foreach (var envVar in envVars)
{ {
startInfo.Environment[envVar.Key] = envVar.Value; startInfo.Environment[envVar.Key] = envVar.Value;
} }
_onProcessConfigure?.Invoke(startInfo); onProcessConfigure?.Invoke(startInfo);
try try
{ {
_process = Process.Start(startInfo); process = Process.Start(startInfo);
_process.EnableRaisingEvents = true; process.EnableRaisingEvents = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -185,7 +185,7 @@ public class ProcessProxy
AppDomain.CurrentDomain.ProcessExit += UnloadHandler; AppDomain.CurrentDomain.ProcessExit += UnloadHandler;
AppDomain.CurrentDomain.UnhandledException += UnloadHandler; AppDomain.CurrentDomain.UnhandledException += UnloadHandler;
return _process; return process;
} }
@@ -197,13 +197,13 @@ public class ProcessProxy
void AttachLogger(bool isStream = false) void AttachLogger(bool isStream = false)
{ {
_stdOut = new EventedStreamReader(_process.StandardOutput); stdOut = new EventedStreamReader(process.StandardOutput);
_stdErr = new EventedStreamReader(_process.StandardError); stdErr = new EventedStreamReader(process.StandardError);
_stdOut.OnReceivedLine += line => WriteToLog(line); stdOut.OnReceivedLine += line => WriteToLog(line);
_stdOut.OnReceivedChunk += chunk => WriteToLog(chunk); stdOut.OnReceivedChunk += chunk => WriteToLog(chunk);
_stdErr.OnReceivedLine += line => WriteToLog(line, true); stdErr.OnReceivedLine += line => WriteToLog(line, true);
_stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true); stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true);
} }
@@ -216,11 +216,11 @@ public class ProcessProxy
line = line.StartsWith("<s>") ? line.Substring(3) : line; line = line.StartsWith("<s>") ? line.Substring(3) : line;
if (_captureLog != null) if (captureLog != null)
{ {
_captureLog(line, isError); captureLog(line, isError);
} }
if (_forwardLog) if (forwardLog)
{ {
(isError ? Console.Error : Console.Out).WriteLine(line); (isError ? Console.Error : Console.Out).WriteLine(line);
} }
@@ -236,11 +236,11 @@ public class ProcessProxy
return; return;
} }
if (_captureLog != null) if (captureLog != null)
{ {
_captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError); captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError);
} }
if (_forwardLog) if (forwardLog)
{ {
(isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count); (isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count);
} }
+8 -10
View File
@@ -1,22 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ViteProxy</RootNamespace> <RootNamespace>ViteProxy</RootNamespace>
<AssemblyName>ViteProxy</AssemblyName> <AssemblyName>ViteProxy</AssemblyName>
<Version>1.3.0</Version> <Version>0.0.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Include="../viteproxy-logo.png" Pack="true" PackagePath="\" />
<None Include="../README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<Product>ViteProxy</Product> <Product>ViteProxy</Product>
<Title>ViteProxy</Title> <Title>ViteProxy</Title>
<Copyright>Copyright © Tobias Klika 2026</Copyright> <Copyright>Copyright © Tobias Klika 2022</Copyright>
<Tags>Vite, npm, middleware, server, development</Tags> <Tags>Vite, npm, middleware, server, development</Tags>
<Authors>Tobias Klika</Authors> <Authors>Tobias Klika</Authors>
<Description>ViteProxy is a proxy for vite projects within ASP.NET Core.</Description> <Description>ViteProxy is a proxy for vite projects within ASP.NET Core.</Description>
@@ -25,8 +20,7 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageId>ViteProxy</PackageId> <PackageId>ViteProxy</PackageId>
<PackageProjectUrl>https://github.com/ceee/ViteProxy</PackageProjectUrl> <PackageProjectUrl>https://github.com/ceee/ViteProxy</PackageProjectUrl>
<PackageIcon>viteproxy-logo.png</PackageIcon> <PackageIconUrl>https://raw.githubusercontent.com/ceee/ViteProxy/main/viteproxy-logo.png</PackageIconUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
@@ -37,5 +31,9 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
</ItemGroup>
</Project> </Project>
+15 -26
View File
@@ -2,55 +2,44 @@
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace ViteProxy; namespace ViteProxy;
public static class ViteProxyApplicationBuilderExtensions public static class ViteProxyApplicationBuilderExtensions
{ {
/// <summary> public static IApplicationBuilder UseViteStaticFiles(this IApplicationBuilder app)
/// Provides static files from the vite working directory or a custom directory or from the named vite settings
/// </summary>
/// <param name="app">The app builder</param>
/// <param name="path">Provide a file path for static file delivery</param>
/// <param name="configName">When vite proxy is added via named options these can be used here for correct retrieval of the vite working directory</param>
/// <returns>The app builde</returns>
public static IApplicationBuilder UseViteStaticFiles(this IApplicationBuilder app, string path = null, string configName = null)
{ {
IWebHostEnvironment env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); IWebHostEnvironment env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
IOptions<ViteProxyOptions> options = app.ApplicationServices.GetRequiredService<IOptions<ViteProxyOptions>>();
string workingDirectory = (options.Value.WorkingDirectory ?? String.Empty).Trim('/');
// do not provide static files when not in development if (workingDirectory == null)
if (!env.IsDevelopment())
{ {
return app; return app;
} }
if (path == null && configName == null) app.UseStaticFiles(new StaticFileOptions
{ {
IOptions<ViteProxyOptions> options = app.ApplicationServices.GetRequiredService<IOptions<ViteProxyOptions>>(); FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, workingDirectory))
path = options.Value.WorkingDirectory; });
}
if (configName != null) return app;
{ }
using IServiceScope scope = app.ApplicationServices.CreateScope();
IOptionsSnapshot<ViteProxyOptions> optionsSnapschat = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<ViteProxyOptions>>();
ViteProxyOptions options = optionsSnapschat.Get(configName); public static IApplicationBuilder UseViteStaticFiles(this IApplicationBuilder app, string path)
path = options.WorkingDirectory; {
} IWebHostEnvironment env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
if (path == null) if (path == null)
{ {
return app; return app;
} }
// TODO build full path
ViteWorkingDirectory workingDirectory = new(env, path);
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
FileProvider = new PhysicalFileProvider(workingDirectory.Path) FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, path))
}); });
return app; return app;
+1 -1
View File
@@ -15,7 +15,7 @@ public class ViteProxyOptions
/// <summary> /// <summary>
/// Directory where the frontend files are located /// Directory where the frontend files are located
/// </summary> /// </summary>
public string WorkingDirectory { get; set; } = "App"; public string WorkingDirectory { get; set; }
/// <summary> /// <summary>
/// Whether to enable the proxy or not /// Whether to enable the proxy or not
+25 -25
View File
@@ -7,38 +7,39 @@ namespace ViteProxy;
public class ViteProxyService : IHostedService public class ViteProxyService : IHostedService
{ {
readonly ViteProxyOptions _options; ViteProxyOptions options;
readonly ViteWorkingDirectory _workingDirectory; ILogger<ViteProxyService> logger;
readonly ILogger<ViteProxyService> _logger; string workingDirectory;
readonly IWebHostEnvironment _env; bool isRunning = false;
bool _isRunning = false;
public ViteProxyService(IWebHostEnvironment env, ILogger<ViteProxyService> logger, IOptions<ViteProxyOptions> options) : this(env, logger, options.Value ?? new()) { } public ViteProxyService(IWebHostEnvironment env, ILogger<ViteProxyService> logger, IOptions<ViteProxyOptions> options)
public ViteProxyService(IWebHostEnvironment env, ILogger<ViteProxyService> logger, ViteProxyOptions options)
{ {
_options = options ?? new(); this.options = options.Value ?? new();
_workingDirectory = new ViteWorkingDirectory(env, _options.WorkingDirectory); this.workingDirectory = Path.Combine(env.ContentRootPath, (this.options.WorkingDirectory ?? String.Empty).Trim('/'));
_env = env; this.logger = logger;
_logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
if (!_options.Enabled || !_env.IsDevelopment()) if (!this.options.Enabled)
{ {
return; return;
} }
// locate npm version and throw if it is not installed // locate npm version and throw if it is not installed
Version npmVersion = await FindNpmVersion() ?? throw new Exception("Please install node+npm to use the vite dev service (https://www.npmjs.com/)"); Version npmVersion = await FindNpmVersion();
if (npmVersion == null)
{
throw new Exception("Please install node+npm to use the vite dev service (https://www.npmjs.com/)");
}
// start vite server // start vite server
ProcessProxy viteProcess = await StartDevServer(_options.Port); ProcessProxy viteProcess = await StartDevServer(options.Port);
_logger.LogInformation("vite listening on: http://localhost:{port} (path: {workingDirectory})", _options.Port, _workingDirectory.Path); logger.LogInformation("vite listening on: http://localhost:{port}", options.Port);
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
@@ -54,7 +55,7 @@ public class ViteProxyService : IHostedService
{ {
Version version = null; Version version = null;
ProcessProxy process = new ProcessProxy(_workingDirectory.Path, "npm").Argument("-v").Capture((value, err) => ProcessProxy process = new ProcessProxy(workingDirectory, "npm").Argument("-v").Capture((value, err) =>
{ {
if (version == null && !value.Contains("not recognized") && Version.TryParse(value, out Version _version)) if (version == null && !value.Contains("not recognized") && Version.TryParse(value, out Version _version))
{ {
@@ -78,15 +79,14 @@ public class ViteProxyService : IHostedService
PidUtils.KillPort((ushort)port, true); PidUtils.KillPort((ushort)port, true);
// create and run the vite script // create and run the vite script
ProcessProxy process = new ProcessProxy(_workingDirectory.Path, "npm", _options.ForwardLog) ProcessProxy process = new ProcessProxy(workingDirectory, "npm", options.ForwardLog)
.Argument("run " + _options.ScriptName) .Argument("run " + options.ScriptName)
.EnvVar("PORT", port.ToString()) .EnvVar("PORT", port.ToString())
//.EnvVar("URL_PREFIX", "/@viteproxy/")
.Capture(CaptureLog); .Capture(CaptureLog);
await process.RunAsync(_options.StartupCondition, TimeSpan.FromSeconds(_options.TimeoutInSeconds)); await process.RunAsync(options.StartupCondition, TimeSpan.FromSeconds(options.TimeoutInSeconds));
_isRunning = true; isRunning = true;
return process; return process;
} }
@@ -94,18 +94,18 @@ public class ViteProxyService : IHostedService
void CaptureLog(string line, bool isError) void CaptureLog(string line, bool isError)
{ {
if (!_isRunning) if (!isRunning)
{ {
return; return;
} }
if (isError) if (isError)
{ {
_logger.LogWarning(line); logger.LogWarning(line);
} }
else else
{ {
_logger.LogInformation(line); logger.LogInformation(line);
} }
} }
} }
+8 -15
View File
@@ -1,46 +1,39 @@
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ViteProxy; namespace ViteProxy;
public static class ViteProxyServiceCollectionExtensions public static class ViteProxyServiceCollectionExtensions
{ {
public static IServiceCollection AddViteProxy(this IServiceCollection services, string configSectionPath = "Vite") public static IServiceCollection AddViteProxy(this IServiceCollection services)
{ {
services.AddLogging(); services.AddLogging();
services.AddOptions(); services.AddOptions();
services.AddHostedService<ViteProxyService>(); services.AddHostedService<ViteProxyService>();
services.AddOptions<ViteProxyOptions>().BindConfiguration(configSectionPath); services.AddOptions<ViteProxyOptions>().BindConfiguration("Vite");
return services; return services;
} }
public static IServiceCollection AddViteProxy(this IServiceCollection services, Action<ViteProxyOptions> configure, string configSectionPath = "Vite") public static IServiceCollection AddViteProxy(this IServiceCollection services, Action<ViteProxyOptions> configure)
{ {
services.AddLogging(); services.AddLogging();
services.AddOptions(); services.AddOptions();
services.AddHostedService<ViteProxyService>(); services.AddHostedService<ViteProxyService>();
services.AddOptions<ViteProxyOptions>().BindConfiguration(configSectionPath); services.AddOptions<ViteProxyOptions>().BindConfiguration("Vite");
services.PostConfigure(configure); services.PostConfigure(configure);
return services; return services;
} }
public static IServiceCollection AddViteProxy(this IServiceCollection services, string configName, IConfiguration namedConfigurationSection) public static IServiceCollection AddViteProxy(this IServiceCollection services, IConfiguration namedConfigurationSection)
{ {
services.AddLogging(); services.AddLogging();
services.AddOptions(); services.AddOptions();
services.AddHostedService((svc) => new ViteProxyService( services.AddHostedService<ViteProxyService>();
env: svc.GetService<IWebHostEnvironment>(), services.AddOptions<ViteProxyOptions>().Bind(namedConfigurationSection);
logger: svc.GetService<ILogger<ViteProxyService>>(),
options: svc.GetService<IOptionsSnapshot<ViteProxyOptions>>().Get(configName)
));
services.AddOptions<ViteProxyOptions>(configName).Bind(namedConfigurationSection);
return services; return services;
} }
-25
View File
@@ -1,25 +0,0 @@
using Microsoft.AspNetCore.Hosting;
namespace ViteProxy;
internal class ViteWorkingDirectory
{
public string Path { get; protected set; }
public string InputPath { get; protected set; }
public ViteWorkingDirectory(IWebHostEnvironment env, string inputPath)
{
InputPath = inputPath;
string path = inputPath.Replace('\\', '/').Trim().TrimEnd('/');
if (System.IO.Path.IsPathFullyQualified(path))
{
Path = path;
}
else
{
Path = System.IO.Path.Combine(env.ContentRootPath, path);
}
}
}