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

Commit c8b337e

Browse files
Add new Microsoft.AspNetCore.SpaServices.Extensions package to host new runtime functionality needed for updated templates until 2.1 ships
1 parent 7bf5516 commit c8b337e

20 files changed

+1520
-1
lines changed

JavaScriptServices.sln

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26730.0
4+
VisualStudioVersion = 15.0.26730.16
55
MinimumVisualStudioVersion = 15.0.26730.03
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{27304DDE-AFB2-4F8B-B765-E3E2F11E886C}"
77
ProjectSection(SolutionItems) = preProject
@@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
3737
Directory.Build.targets = Directory.Build.targets
3838
EndProjectSection
3939
EndProject
40+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions", "src\Microsoft.AspNetCore.SpaServices.Extensions\Microsoft.AspNetCore.SpaServices.Extensions.csproj", "{D40BD1C4-6A6F-4213-8535-1057F3EB3400}"
41+
EndProject
4042
Global
4143
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4244
Debug|Any CPU = Debug|Any CPU
@@ -67,6 +69,10 @@ Global
6769
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
6870
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
6971
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Release|Any CPU.Build.0 = Release|Any CPU
72+
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
73+
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Debug|Any CPU.Build.0 = Debug|Any CPU
74+
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Release|Any CPU.ActiveCfg = Release|Any CPU
75+
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Release|Any CPU.Build.0 = Release|Any CPU
7076
EndGlobalSection
7177
GlobalSection(SolutionProperties) = preSolution
7278
HideSolutionNode = FALSE
@@ -79,6 +85,7 @@ Global
7985
{1931B19A-EC42-4D56-B2D0-FB06D17244DA} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
8086
{DE479DC3-1461-4EAD-A188-4AF7AA4AE344} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
8187
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
88+
{D40BD1C4-6A6F-4213-8535-1057F3EB3400} = {27304DDE-AFB2-4F8B-B765-E3E2F11E886C}
8289
EndGlobalSection
8390
GlobalSection(ExtensibilityGlobals) = postSolution
8491
SolutionGuid = {DDF59B0D-2DEC-45D6-8667-DCB767487101}

build/dependencies.props

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
1414
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreServerKestrelPackageVersion>
1515
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreStaticFilesPackageVersion>
16+
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreWebSocketsPackageVersion>
1617
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsDependencyInjectionPackageVersion>
1718
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsLoggingConsolePackageVersion>
1819
<MicrosoftExtensionsLoggingDebugPackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsLoggingDebugPackageVersion>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.NodeServices.Npm;
6+
using Microsoft.AspNetCore.NodeServices.Util;
7+
using Microsoft.AspNetCore.SpaServices.Prerendering;
8+
using System;
9+
using System.IO;
10+
using System.Text.RegularExpressions;
11+
using System.Threading.Tasks;
12+
13+
namespace Microsoft.AspNetCore.SpaServices.AngularCli
14+
{
15+
/// <summary>
16+
/// Provides an implementation of <see cref="ISpaPrerendererBuilder"/> that can build
17+
/// an Angular application by invoking the Angular CLI.
18+
/// </summary>
19+
public class AngularCliBuilder : ISpaPrerendererBuilder
20+
{
21+
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
22+
private static TimeSpan BuildTimeout = TimeSpan.FromSeconds(50); // Note that the HTTP request itself by default times out after 60s, so you only get useful error information if this is shorter
23+
24+
private readonly string _npmScriptName;
25+
26+
/// <summary>
27+
/// Constructs an instance of <see cref="AngularCliBuilder"/>.
28+
/// </summary>
29+
/// <param name="npmScript">The name of the script in your package.json file that builds the server-side bundle for your Angular application.</param>
30+
public AngularCliBuilder(string npmScript)
31+
{
32+
if (string.IsNullOrEmpty(npmScript))
33+
{
34+
throw new ArgumentException("Cannot be null or empty.", nameof(npmScript));
35+
}
36+
37+
_npmScriptName = npmScript;
38+
}
39+
40+
/// <inheritdoc />
41+
public Task Build(ISpaBuilder spaBuilder)
42+
{
43+
var sourcePath = spaBuilder.Options.SourcePath;
44+
if (string.IsNullOrEmpty(sourcePath))
45+
{
46+
throw new InvalidOperationException($"To use {nameof(AngularCliBuilder)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
47+
}
48+
49+
var logger = AngularCliMiddleware.GetOrCreateLogger(spaBuilder.ApplicationBuilder);
50+
var npmScriptRunner = new NpmScriptRunner(
51+
sourcePath,
52+
_npmScriptName,
53+
"--watch");
54+
npmScriptRunner.AttachToLogger(logger);
55+
56+
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
57+
{
58+
try
59+
{
60+
return npmScriptRunner.StdOut.WaitForMatch(
61+
new Regex("chunk", RegexOptions.None, RegexMatchTimeout),
62+
BuildTimeout);
63+
}
64+
catch (EndOfStreamException ex)
65+
{
66+
throw new InvalidOperationException(
67+
$"The NPM script '{_npmScriptName}' exited without indicating success. " +
68+
$"Error output was: {stdErrReader.ReadAsString()}", ex);
69+
}
70+
}
71+
}
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using System;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.NodeServices.Npm;
8+
using System.Text.RegularExpressions;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging.Console;
12+
using System.Net.Sockets;
13+
using System.Net;
14+
using System.IO;
15+
using Microsoft.AspNetCore.NodeServices.Util;
16+
17+
namespace Microsoft.AspNetCore.SpaServices.AngularCli
18+
{
19+
internal static class AngularCliMiddleware
20+
{
21+
private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices";
22+
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
23+
private static TimeSpan StartupTimeout = TimeSpan.FromSeconds(50); // Note that the HTTP request itself by default times out after 60s, so you only get useful error information if this is shorter
24+
25+
public static void Attach(
26+
ISpaBuilder spaBuilder,
27+
string npmScriptName)
28+
{
29+
var sourcePath = spaBuilder.Options.SourcePath;
30+
if (string.IsNullOrEmpty(sourcePath))
31+
{
32+
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
33+
}
34+
35+
if (string.IsNullOrEmpty(npmScriptName))
36+
{
37+
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
38+
}
39+
40+
// Start Angular CLI and attach to middleware pipeline
41+
var appBuilder = spaBuilder.ApplicationBuilder;
42+
var logger = GetOrCreateLogger(appBuilder);
43+
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger);
44+
45+
// Everything we proxy is hardcoded to target http://localhost because:
46+
// - the requests are always from the local machine (we're not accepting remote
47+
// requests that go directly to the Angular CLI middleware server)
48+
// - given that, there's no reason to use https, and we couldn't even if we
49+
// wanted to, because in general the Angular CLI server has no certificate
50+
var targetUriTask = angularCliServerInfoTask.ContinueWith(
51+
task => new UriBuilder("http", "localhost", task.Result.Port).Uri);
52+
53+
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, targetUriTask);
54+
}
55+
56+
internal static ILogger GetOrCreateLogger(IApplicationBuilder appBuilder)
57+
{
58+
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
59+
var loggerFactory = appBuilder.ApplicationServices.GetService<ILoggerFactory>();
60+
var logger = loggerFactory != null
61+
? loggerFactory.CreateLogger(LogCategoryName)
62+
: new ConsoleLogger(LogCategoryName, null, false);
63+
return logger;
64+
}
65+
66+
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
67+
string sourcePath, string npmScriptName, ILogger logger)
68+
{
69+
var portNumber = FindAvailablePort();
70+
logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
71+
72+
var npmScriptRunner = new NpmScriptRunner(
73+
sourcePath, npmScriptName, $"--port {portNumber}");
74+
npmScriptRunner.AttachToLogger(logger);
75+
76+
Match openBrowserLine;
77+
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
78+
{
79+
try
80+
{
81+
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch(
82+
new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout),
83+
StartupTimeout);
84+
}
85+
catch (EndOfStreamException ex)
86+
{
87+
throw new InvalidOperationException(
88+
$"The NPM script '{npmScriptName}' exited without indicating that the " +
89+
$"Angular CLI was listening for requests. The error output was: " +
90+
$"{stdErrReader.ReadAsString()}", ex);
91+
}
92+
catch (TaskCanceledException ex)
93+
{
94+
throw new InvalidOperationException(
95+
$"The Angular CLI process did not start listening for requests " +
96+
$"within the timeout period of {StartupTimeout.Seconds} seconds. " +
97+
$"Check the log output for error information.", ex);
98+
}
99+
}
100+
101+
var uri = new Uri(openBrowserLine.Groups[1].Value);
102+
var serverInfo = new AngularCliServerInfo { Port = uri.Port };
103+
104+
// Even after the Angular CLI claims to be listening for requests, there's a short
105+
// period where it will give an error if you make a request too quickly. Give it
106+
// a moment to finish starting up.
107+
await Task.Delay(500);
108+
109+
return serverInfo;
110+
}
111+
112+
private static int FindAvailablePort()
113+
{
114+
var listener = new TcpListener(IPAddress.Loopback, 0);
115+
listener.Start();
116+
try
117+
{
118+
return ((IPEndPoint)listener.LocalEndpoint).Port;
119+
}
120+
finally
121+
{
122+
listener.Stop();
123+
}
124+
}
125+
126+
class AngularCliServerInfo
127+
{
128+
public int Port { get; set; }
129+
}
130+
}
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using System;
6+
7+
namespace Microsoft.AspNetCore.SpaServices.AngularCli
8+
{
9+
/// <summary>
10+
/// Extension methods for enabling Angular CLI middleware support.
11+
/// </summary>
12+
public static class AngularCliMiddlewareExtensions
13+
{
14+
/// <summary>
15+
/// Handles requests by passing them through to an instance of the Angular CLI server.
16+
/// This means you can always serve up-to-date CLI-built resources without having
17+
/// to run the Angular CLI server manually.
18+
///
19+
/// This feature should only be used in development. For production deployments, be
20+
/// sure not to enable the Angular CLI server.
21+
/// </summary>
22+
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
23+
/// <param name="npmScript">The name of the script in your package.json file that launches the Angular CLI process.</param>
24+
public static void UseAngularCliServer(
25+
this ISpaBuilder spaBuilder,
26+
string npmScript)
27+
{
28+
if (spaBuilder == null)
29+
{
30+
throw new ArgumentNullException(nameof(spaBuilder));
31+
}
32+
33+
var spaOptions = spaBuilder.Options;
34+
35+
if (string.IsNullOrEmpty(spaOptions.SourcePath))
36+
{
37+
throw new InvalidOperationException($"To use {nameof(UseAngularCliServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
38+
}
39+
40+
AngularCliMiddleware.Attach(spaBuilder, npmScript);
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using System;
6+
7+
namespace Microsoft.AspNetCore.SpaServices
8+
{
9+
internal class DefaultSpaBuilder : ISpaBuilder
10+
{
11+
public IApplicationBuilder ApplicationBuilder { get; }
12+
13+
public SpaOptions Options { get; }
14+
15+
public DefaultSpaBuilder(IApplicationBuilder applicationBuilder, SpaOptions options)
16+
{
17+
ApplicationBuilder = applicationBuilder
18+
?? throw new ArgumentNullException(nameof(applicationBuilder));
19+
20+
Options = options
21+
?? throw new ArgumentNullException(nameof(options));
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
6+
namespace Microsoft.AspNetCore.SpaServices
7+
{
8+
/// <summary>
9+
/// Defines a class that provides mechanisms for configuring the hosting
10+
/// of a Single Page Application (SPA) and attaching middleware.
11+
/// </summary>
12+
public interface ISpaBuilder
13+
{
14+
/// <summary>
15+
/// The <see cref="IApplicationBuilder"/> representing the middleware pipeline
16+
/// in which the SPA is being hosted.
17+
/// </summary>
18+
IApplicationBuilder ApplicationBuilder { get; }
19+
20+
/// <summary>
21+
/// Describes configuration options for hosting a SPA.
22+
/// </summary>
23+
SpaOptions Options { get; }
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Description>Helpers for building single-page applications on ASP.NET MVC Core.</Description>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
14+
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="$(MicrosoftAspNetCoreWebSocketsPackageVersion)" />
15+
</ItemGroup>
16+
17+
</Project>

0 commit comments

Comments
 (0)