Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Add VueCli Middleware #1726

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.NodeServices.Npm;
using Microsoft.AspNetCore.NodeServices.Util;
using Microsoft.AspNetCore.SpaServices.Util;
using System;
using System.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SpaServices.Extensions.Util;

namespace Microsoft.AspNetCore.SpaServices.VueCli
{
internal static class VueCliMiddleware
{
private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices";
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine

public static void Attach(
ISpaBuilder spaBuilder,
string npmScriptName, int port = 0)
{
var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath))
{
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
}

if (string.IsNullOrEmpty(npmScriptName))
{
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
}

// Start vue-cli and attach to middleware pipeline
var appBuilder = spaBuilder.ApplicationBuilder;
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
var portTask = StartVueCliServerAsync(sourcePath, npmScriptName, logger, port);

// Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote
// requests that go directly to the vue-cli server)
// - given that, there's no reason to use https, and we couldn't even if we
// wanted to, because in general the vue-cli server has no certificate
var targetUriTask = portTask.ContinueWith(
task =>
new UriBuilder("http", "localhost", task.Result).Uri);

SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
{
// On each request, we create a separate startup task with its own timeout. That way, even if
// the first request times out, subsequent requests could still work.
var timeout = spaBuilder.Options.StartupTimeout;
return targetUriTask.WithTimeout(timeout,
$"The vue-cli server did not start listening for requests " +
$"within the timeout period of {timeout.Seconds} seconds. " +
$"Check the log output for error information.");
});
}

private static async Task<int> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger, int portNumber)
{
if (portNumber < 80)
portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting server on port {portNumber}...");

var envVars = new Dictionary<string, string>
{
{ "PORT", portNumber.ToString() },
{ "DEV_SERVER_PORT", portNumber.ToString() }, // vue cli 3 uses --port {number}, included below
{ "BROWSER", "none" }, // We don't want vue-cli to open its own extra browser window pointing to the internal dev server port
};
var npmScriptRunner = new NpmScriptRunner(sourcePath, npmScriptName, $"--port {portNumber}", envVars);
npmScriptRunner.AttachToLogger(logger);

using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
{
try
{
// Although the Vue dev server may eventually tell us the URL it's listening on,
// it doesn't do so until it's finished compiling, and even then only if there were
// no compiler warnings. So instead of waiting for that, consider it ready as soon
// as it starts listening for requests.
await npmScriptRunner.StdOut.WaitForMatch(
new Regex("running at", RegexOptions.None, RegexMatchTimeout));
}
catch (EndOfStreamException ex)
{
throw new InvalidOperationException(
$"The NPM script '{npmScriptName}' exited without indicating that the " +
$"server was listening for requests. The error output was: " +
$"{stdErrReader.ReadAsString()}", ex);
}
}

return portNumber;
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.Builder;
using System;

namespace Microsoft.AspNetCore.SpaServices.VueCli
{
/// <summary>
/// Extension methods for enabling Vue development server middleware support.
/// </summary>
public static class VueCliMiddlewareExtensions
{
/// <summary>
/// Handles requests by passing them through to an instance of the vue-cli server.
/// This means you can always serve up-to-date CLI-built resources without having
/// to run the vue-cli server manually.
///
/// This feature should only be used in development. For production deployments, be
/// sure not to enable the vue-cli server.
/// </summary>
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
/// <param name="npmScript">The name of the script in your package.json file that launches the vue-cli server.</param>
/// <param name="port">Specify vue cli server port number. If &lt; 80, uses random port. </param>
public static void UseVueCli(
this ISpaBuilder spaBuilder,
string npmScript, int port = 0)
{
if (spaBuilder == null)
{
throw new ArgumentNullException(nameof(spaBuilder));
}

var spaOptions = spaBuilder.Options;

if (string.IsNullOrEmpty(spaOptions.SourcePath))
{
throw new InvalidOperationException($"To use {nameof(UseVueCli)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
}

VueCliMiddleware.Attach(spaBuilder, npmScript, port);
}
}
}