Skip to content
This repository was archived by the owner on Nov 6, 2023. It is now read-only.

Commit e293d5a

Browse files
committed
Update to allow custom regex.
Very experimental Yarn support (not recommended). Remove submodule to depreciated https://github.com/aspnet/JavaScriptServices
1 parent 5e85271 commit e293d5a

File tree

7 files changed

+389
-29
lines changed

7 files changed

+389
-29
lines changed

.gitmodules

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
[submodule "JavaScriptServices"]
2-
path = JavaScriptServices
3-
url = https://github.com/aspnet/JavaScriptServices
4-
branch = release/2.1
5-
[submodule "submodules/JavaScriptServices"]
6-
path = submodules/JavaScriptServices
7-
url = https://github.com/aspnet/JavaScriptServices
8-
branch = release/2.1
1+
[submodule "JavaScriptServices"]
2+
path = JavaScriptServices
3+
url = https://github.com/aspnet/JavaScriptServices
4+
branch = release/2.1

src/Util/Internals.cs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright (c) .NET Foundation Contributors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Original Source: https://github.com/aspnet/JavaScriptServices
4+
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Console;
9+
using System;
10+
using System.Threading.Tasks;
11+
using System.Net;
12+
using System.Net.Sockets;
13+
using System.IO;
14+
using System.Text;
15+
using System.Text.RegularExpressions;
16+
17+
namespace VueCliMiddleware
18+
{
19+
internal static class LoggerFinder
20+
{
21+
public static ILogger GetOrCreateLogger(
22+
IApplicationBuilder appBuilder,
23+
string logCategoryName)
24+
{
25+
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
26+
var loggerFactory = appBuilder.ApplicationServices.GetService<ILoggerFactory>();
27+
var logger = loggerFactory != null
28+
? loggerFactory.CreateLogger(logCategoryName)
29+
: new ConsoleLogger(logCategoryName, null, false);
30+
return logger;
31+
}
32+
}
33+
34+
internal static class TaskTimeoutExtensions
35+
{
36+
public static async Task WithTimeout(this Task task, TimeSpan timeoutDelay, string message)
37+
{
38+
if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
39+
{
40+
task.Wait(); // Allow any errors to propagate
41+
}
42+
else
43+
{
44+
throw new TimeoutException(message);
45+
}
46+
}
47+
48+
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeoutDelay, string message)
49+
{
50+
if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
51+
{
52+
return task.Result;
53+
}
54+
else
55+
{
56+
throw new TimeoutException(message);
57+
}
58+
}
59+
}
60+
61+
internal static class TcpPortFinder
62+
{
63+
public static int FindAvailablePort()
64+
{
65+
var listener = new TcpListener(IPAddress.Loopback, 0);
66+
listener.Start();
67+
try
68+
{
69+
return ((IPEndPoint)listener.LocalEndpoint).Port;
70+
}
71+
finally
72+
{
73+
listener.Stop();
74+
}
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Wraps a <see cref="StreamReader"/> to expose an evented API, issuing notifications
80+
/// when the stream emits partial lines, completed lines, or finally closes.
81+
/// </summary>
82+
internal class EventedStreamReader
83+
{
84+
public delegate void OnReceivedChunkHandler(ArraySegment<char> chunk);
85+
public delegate void OnReceivedLineHandler(string line);
86+
public delegate void OnStreamClosedHandler();
87+
88+
public event OnReceivedChunkHandler OnReceivedChunk;
89+
public event OnReceivedLineHandler OnReceivedLine;
90+
public event OnStreamClosedHandler OnStreamClosed;
91+
92+
private readonly StreamReader _streamReader;
93+
private readonly StringBuilder _linesBuffer;
94+
95+
public EventedStreamReader(StreamReader streamReader)
96+
{
97+
_streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader));
98+
_linesBuffer = new StringBuilder();
99+
Task.Factory.StartNew(Run);
100+
}
101+
102+
public Task<Match> WaitForMatch(Regex regex)
103+
{
104+
var tcs = new TaskCompletionSource<Match>();
105+
var completionLock = new object();
106+
107+
OnReceivedLineHandler onReceivedLineHandler = null;
108+
OnStreamClosedHandler onStreamClosedHandler = null;
109+
110+
void ResolveIfStillPending(Action applyResolution)
111+
{
112+
lock (completionLock)
113+
{
114+
if (!tcs.Task.IsCompleted)
115+
{
116+
OnReceivedLine -= onReceivedLineHandler;
117+
OnStreamClosed -= onStreamClosedHandler;
118+
applyResolution();
119+
}
120+
}
121+
}
122+
123+
onReceivedLineHandler = line =>
124+
{
125+
var match = regex.Match(line);
126+
if (match.Success)
127+
{
128+
ResolveIfStillPending(() => tcs.SetResult(match));
129+
}
130+
};
131+
132+
onStreamClosedHandler = () =>
133+
{
134+
ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));
135+
};
136+
137+
OnReceivedLine += onReceivedLineHandler;
138+
OnStreamClosed += onStreamClosedHandler;
139+
140+
return tcs.Task;
141+
}
142+
143+
private async Task Run()
144+
{
145+
var buf = new char[8 * 1024];
146+
while (true)
147+
{
148+
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
149+
if (chunkLength == 0)
150+
{
151+
OnClosed();
152+
break;
153+
}
154+
155+
OnChunk(new ArraySegment<char>(buf, 0, chunkLength));
156+
157+
var lineBreakPos = Array.IndexOf(buf, '\n', 0, chunkLength);
158+
if (lineBreakPos < 0)
159+
{
160+
_linesBuffer.Append(buf, 0, chunkLength);
161+
}
162+
else
163+
{
164+
_linesBuffer.Append(buf, 0, lineBreakPos + 1);
165+
OnCompleteLine(_linesBuffer.ToString());
166+
_linesBuffer.Clear();
167+
_linesBuffer.Append(buf, lineBreakPos + 1, chunkLength - (lineBreakPos + 1));
168+
}
169+
}
170+
}
171+
172+
private void OnChunk(ArraySegment<char> chunk)
173+
{
174+
var dlg = OnReceivedChunk;
175+
dlg?.Invoke(chunk);
176+
}
177+
178+
private void OnCompleteLine(string line)
179+
{
180+
var dlg = OnReceivedLine;
181+
dlg?.Invoke(line);
182+
}
183+
184+
private void OnClosed()
185+
{
186+
var dlg = OnStreamClosed;
187+
dlg?.Invoke();
188+
}
189+
}
190+
191+
/// <summary>
192+
/// Captures the completed-line notifications from a <see cref="EventedStreamReader"/>,
193+
/// combining the data into a single <see cref="string"/>.
194+
/// </summary>
195+
internal class EventedStreamStringReader : IDisposable
196+
{
197+
private EventedStreamReader _eventedStreamReader;
198+
private bool _isDisposed;
199+
private StringBuilder _stringBuilder = new StringBuilder();
200+
201+
public EventedStreamStringReader(EventedStreamReader eventedStreamReader)
202+
{
203+
_eventedStreamReader = eventedStreamReader
204+
?? throw new ArgumentNullException(nameof(eventedStreamReader));
205+
_eventedStreamReader.OnReceivedLine += OnReceivedLine;
206+
}
207+
208+
public string ReadAsString() => _stringBuilder.ToString();
209+
210+
private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line);
211+
212+
public void Dispose()
213+
{
214+
if (!_isDisposed)
215+
{
216+
_eventedStreamReader.OnReceivedLine -= OnReceivedLine;
217+
_isDisposed = true;
218+
}
219+
}
220+
}
221+
}

src/Util/ScriptRunner.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) .NET Foundation Contributors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Original Source: https://github.com/aspnet/JavaScriptServices
4+
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Diagnostics;
8+
using System.Runtime.InteropServices;
9+
using System.Text.RegularExpressions;
10+
using System.Collections.Generic;
11+
12+
namespace VueCliMiddleware
13+
{
14+
/// <summary>
15+
/// Executes the <c>script</c> entries defined in a <c>package.json</c> file,
16+
/// capturing any output written to stdio.
17+
/// </summary>
18+
internal class ScriptRunner
19+
{
20+
public EventedStreamReader StdOut { get; }
21+
public EventedStreamReader StdErr { get; }
22+
23+
public ScriptRunnerType Runner { get; }
24+
25+
private string GetExeName() => Runner == ScriptRunnerType.Npm ? "npm" : "yarn";
26+
private string GetArgPrefix() => Runner == ScriptRunnerType.Npm ? "run " : "";
27+
private string GetArgSuffix() => Runner == ScriptRunnerType.Npm ? "-- " : "";
28+
29+
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
30+
31+
public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, ScriptRunnerType runner)
32+
{
33+
if (string.IsNullOrEmpty(workingDirectory))
34+
{
35+
throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
36+
}
37+
38+
if (string.IsNullOrEmpty(scriptName))
39+
{
40+
throw new ArgumentException("Cannot be null or empty.", nameof(scriptName));
41+
}
42+
43+
Runner = runner;
44+
45+
var npmExe = GetExeName();
46+
var completeArguments = $"{GetArgPrefix()}{scriptName} {GetArgSuffix()}{arguments ?? string.Empty}";
47+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
48+
{
49+
// On Windows, the NPM executable is a .cmd file, so it can't be executed
50+
// directly (except with UseShellExecute=true, but that's no good, because
51+
// it prevents capturing stdio). So we need to invoke it via "cmd /c".
52+
npmExe = "cmd";
53+
completeArguments = $"/c npm {completeArguments}";
54+
}
55+
56+
var processStartInfo = new ProcessStartInfo(npmExe)
57+
{
58+
Arguments = completeArguments,
59+
UseShellExecute = false,
60+
RedirectStandardInput = true,
61+
RedirectStandardOutput = true,
62+
RedirectStandardError = true,
63+
WorkingDirectory = workingDirectory
64+
};
65+
66+
if (envVars != null)
67+
{
68+
foreach (var keyValuePair in envVars)
69+
{
70+
processStartInfo.Environment[keyValuePair.Key] = keyValuePair.Value;
71+
}
72+
}
73+
74+
var process = LaunchNodeProcess(processStartInfo);
75+
StdOut = new EventedStreamReader(process.StandardOutput);
76+
StdErr = new EventedStreamReader(process.StandardError);
77+
}
78+
79+
public void AttachToLogger(ILogger logger)
80+
{
81+
// When the NPM task emits complete lines, pass them through to the real logger
82+
StdOut.OnReceivedLine += line =>
83+
{
84+
if (!string.IsNullOrWhiteSpace(line))
85+
{
86+
// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward
87+
// those to loggers (because a logger isn't necessarily any kind of terminal)
88+
logger.LogInformation(StripAnsiColors(line) + "\r\n");
89+
}
90+
};
91+
92+
StdErr.OnReceivedLine += line =>
93+
{
94+
if (!string.IsNullOrWhiteSpace(line))
95+
{
96+
logger.LogError(StripAnsiColors(line + "\r\n"));
97+
}
98+
};
99+
100+
// But when it emits incomplete lines, assume this is progress information and
101+
// hence just pass it through to StdOut regardless of logger config.
102+
StdErr.OnReceivedChunk += chunk =>
103+
{
104+
var containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
105+
106+
if (!containsNewline)
107+
{
108+
Console.Write(chunk.Array, chunk.Offset, chunk.Count);
109+
}
110+
};
111+
}
112+
113+
private static string StripAnsiColors(string line)
114+
=> AnsiColorRegex.Replace(line, string.Empty);
115+
116+
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
117+
{
118+
try
119+
{
120+
var process = Process.Start(startInfo);
121+
122+
// See equivalent comment in OutOfProcessNodeInstance.cs for why
123+
process.EnableRaisingEvents = true;
124+
125+
return process;
126+
}
127+
catch (Exception ex)
128+
{
129+
var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n"
130+
+ $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n"
131+
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
132+
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
133+
+ "[2] See the InnerException for further details of the cause.";
134+
throw new InvalidOperationException(message, ex);
135+
}
136+
}
137+
}
138+
}

src/Util/ScriptRunnerType.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace VueCliMiddleware
2+
{
3+
public enum ScriptRunnerType
4+
{
5+
Npm,
6+
Yarn
7+
}
8+
}

0 commit comments

Comments
 (0)