Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c8b337e

Browse filesBrowse 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
Copy full SHA for c8b337e
Expand file treeCollapse file tree

20 files changed

+1520
-1
lines changed
Open diff view settings
Collapse file

‎JavaScriptServices.sln‎

Copy file name to clipboardExpand all lines: JavaScriptServices.sln
+8-1Lines changed: 8 additions & 1 deletion
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}
Collapse file

‎build/dependencies.props‎

Copy file name to clipboardExpand all lines: build/dependencies.props
+1Lines changed: 1 addition & 0 deletions
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>
Collapse file
+73Lines changed: 73 additions & 0 deletions
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+
}
Collapse file
+131Lines changed: 131 additions & 0 deletions
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+
}
Collapse file
+43Lines changed: 43 additions & 0 deletions
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+
}
Collapse file
+24Lines changed: 24 additions & 0 deletions
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+
}
Collapse file
+25Lines changed: 25 additions & 0 deletions
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+
}
Collapse file
+17Lines changed: 17 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.