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

Commit 3fd1b40

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

13 files changed

+1007
-1
lines changed

JavaScriptServices.sln

Lines 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}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.SpaServices.Prerendering;
6+
using System;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.SpaServices.AngularCli
10+
{
11+
/// <summary>
12+
/// Provides an implementation of <see cref="ISpaPrerendererBuilder"/> that can build
13+
/// an Angular application by invoking the Angular CLI.
14+
/// </summary>
15+
public class AngularCliBuilder : ISpaPrerendererBuilder
16+
{
17+
private readonly string _cliAppName;
18+
19+
/// <summary>
20+
/// Constructs an instance of <see cref="AngularCliBuilder"/>.
21+
/// </summary>
22+
/// <param name="cliAppName">The name of the application to be built. This must match an entry in your <c>.angular-cli.json</c> file.</param>
23+
public AngularCliBuilder(string cliAppName)
24+
{
25+
_cliAppName = cliAppName;
26+
}
27+
28+
/// <inheritdoc />
29+
public Task Build(IApplicationBuilder app)
30+
{
31+
// Locate the AngularCliMiddleware within the provided IApplicationBuilder
32+
if (app.Properties.TryGetValue(
33+
AngularCliMiddleware.AngularCliMiddlewareKey,
34+
out var angularCliMiddleware))
35+
{
36+
return ((AngularCliMiddleware)angularCliMiddleware)
37+
.StartAngularCliBuilderAsync(_cliAppName);
38+
}
39+
else
40+
{
41+
throw new Exception(
42+
$"Cannot use {nameof(AngularCliBuilder)} unless you are also using" +
43+
$" {nameof(AngularCliMiddlewareExtensions.UseAngularCliServer)}.");
44+
}
45+
}
46+
}
47+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.Hosting;
6+
using Microsoft.AspNetCore.NodeServices;
7+
using System;
8+
using System.IO;
9+
using System.Net.Http;
10+
using System.Threading.Tasks;
11+
using System.Threading;
12+
using Microsoft.AspNetCore.SpaServices.Extensions.Proxy;
13+
14+
namespace Microsoft.AspNetCore.SpaServices.AngularCli
15+
{
16+
internal class AngularCliMiddleware
17+
{
18+
private const string _middlewareResourceName = "/Content/Node/angular-cli-middleware.js";
19+
20+
internal readonly static string AngularCliMiddlewareKey = Guid.NewGuid().ToString();
21+
22+
private readonly INodeServices _nodeServices;
23+
private readonly string _middlewareScriptPath;
24+
private readonly HttpClient _neverTimeOutHttpClient =
25+
ConditionalProxy.CreateHttpClientForProxy(Timeout.InfiniteTimeSpan);
26+
27+
public AngularCliMiddleware(
28+
IApplicationBuilder appBuilder,
29+
string sourcePath,
30+
SpaDefaultPageMiddleware defaultPageMiddleware)
31+
{
32+
if (string.IsNullOrEmpty(sourcePath))
33+
{
34+
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
35+
}
36+
37+
// Prepare to make calls into Node
38+
_nodeServices = CreateNodeServicesInstance(appBuilder, sourcePath);
39+
_middlewareScriptPath = GetAngularCliMiddlewareScriptPath(appBuilder);
40+
41+
// Start Angular CLI and attach to middleware pipeline
42+
var angularCliServerInfoTask = StartAngularCliServerAsync();
43+
44+
// Everything we proxy is hardcoded to target http://localhost because:
45+
// - the requests are always from the local machine (we're not accepting remote
46+
// requests that go directly to the Angular CLI middleware server)
47+
// - given that, there's no reason to use https, and we couldn't even if we
48+
// wanted to, because in general the Angular CLI server has no certificate
49+
var proxyOptionsTask = angularCliServerInfoTask.ContinueWith(
50+
task => new ConditionalProxyMiddlewareTarget(
51+
"http", "localhost", task.Result.Port.ToString()));
52+
53+
var applicationStoppingToken = GetStoppingToken(appBuilder);
54+
55+
// Proxy all requests into the Angular CLI server
56+
appBuilder.Use(async (context, next) =>
57+
{
58+
var didProxyRequest = await ConditionalProxy.PerformProxyRequest(
59+
context, _neverTimeOutHttpClient, proxyOptionsTask, applicationStoppingToken);
60+
61+
// Since we are proxying everything, this is the end of the middleware pipeline.
62+
// We won't call next().
63+
if (!didProxyRequest)
64+
{
65+
context.Response.StatusCode = 404;
66+
}
67+
});
68+
69+
// Advertise the availability of this feature to other SPA middleware
70+
appBuilder.Properties.Add(AngularCliMiddlewareKey, this);
71+
}
72+
73+
internal Task StartAngularCliBuilderAsync(string cliAppName)
74+
{
75+
return _nodeServices.InvokeExportAsync<AngularCliServerInfo>(
76+
_middlewareScriptPath,
77+
"startAngularCliBuilder",
78+
cliAppName);
79+
}
80+
81+
private static INodeServices CreateNodeServicesInstance(
82+
IApplicationBuilder appBuilder, string sourcePath)
83+
{
84+
// Unlike other consumers of NodeServices, AngularCliMiddleware dosen't share Node instances, nor does it
85+
// use your DI configuration. It's important for AngularCliMiddleware to have its own private Node instance
86+
// because it must *not* restart when files change (it's designed to watch for changes and rebuild).
87+
var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices)
88+
{
89+
WatchFileExtensions = new string[] { }, // Don't watch anything
90+
ProjectPath = Path.Combine(Directory.GetCurrentDirectory(), sourcePath),
91+
};
92+
93+
if (!Directory.Exists(nodeServicesOptions.ProjectPath))
94+
{
95+
throw new DirectoryNotFoundException($"Directory not found: {nodeServicesOptions.ProjectPath}");
96+
}
97+
98+
return NodeServicesFactory.CreateNodeServices(nodeServicesOptions);
99+
}
100+
101+
private static string GetAngularCliMiddlewareScriptPath(IApplicationBuilder appBuilder)
102+
{
103+
var script = EmbeddedResourceReader.Read(typeof(AngularCliMiddleware), _middlewareResourceName);
104+
var nodeScript = new StringAsTempFile(script, GetStoppingToken(appBuilder));
105+
return nodeScript.FileName;
106+
}
107+
108+
private static CancellationToken GetStoppingToken(IApplicationBuilder appBuilder)
109+
{
110+
var applicationLifetime = appBuilder
111+
.ApplicationServices
112+
.GetService(typeof(IApplicationLifetime));
113+
return ((IApplicationLifetime)applicationLifetime).ApplicationStopping;
114+
}
115+
116+
private async Task<AngularCliServerInfo> StartAngularCliServerAsync()
117+
{
118+
// Tell Node to start the server hosting the Angular CLI
119+
var angularCliServerInfo = await _nodeServices.InvokeExportAsync<AngularCliServerInfo>(
120+
_middlewareScriptPath,
121+
"startAngularCliServer");
122+
123+
// Even after the Angular CLI claims to be listening for requests, there's a short
124+
// period where it will give an error if you make a request too quickly. Give it
125+
// a moment to finish starting up.
126+
await Task.Delay(500);
127+
128+
return angularCliServerInfo;
129+
}
130+
131+
#pragma warning disable CS0649
132+
class AngularCliServerInfo
133+
{
134+
public int Port { get; set; }
135+
}
136+
}
137+
#pragma warning restore CS0649
138+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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="app">The <see cref="IApplicationBuilder"/>.</param>
23+
/// <param name="sourcePath">The disk path, relative to the current directory, of the directory containing the SPA source files. When Angular CLI executes, this will be its working directory.</param>
24+
public static void UseAngularCliServer(
25+
this IApplicationBuilder app,
26+
string sourcePath)
27+
{
28+
var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(app);
29+
if (defaultPageMiddleware == null)
30+
{
31+
throw new Exception($"{nameof(UseAngularCliServer)} should be called inside the 'configue' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
32+
}
33+
34+
new AngularCliMiddleware(app, sourcePath, defaultPageMiddleware);
35+
}
36+
}
37+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
var childProcess = require('child_process');
5+
var net = require('net');
6+
var readline = require('readline');
7+
var url = require('url');
8+
9+
module.exports = {
10+
startAngularCliBuilder: function startAngularCliBuilder(callback, appName) {
11+
var proc = executeAngularCli([
12+
'build',
13+
'-app', appName,
14+
'--watch'
15+
]);
16+
proc.stdout.pipe(process.stdout);
17+
waitForLine(proc.stdout, /chunk/).then(function () {
18+
callback();
19+
});
20+
},
21+
22+
startAngularCliServer: function startAngularCliServer(callback, options) {
23+
getOSAssignedPortNumber().then(function (portNumber) {
24+
// Start @angular/cli dev server on private port, and pipe its output
25+
// back to the ASP.NET host process.
26+
// TODO: Support streaming arbitrary chunks to host process's stdout
27+
// rather than just full lines, so we can see progress being logged
28+
var devServerProc = executeAngularCli([
29+
'serve',
30+
'--port', portNumber.toString(),
31+
'--deploy-url', '/dist/', // Value should come from .angular-cli.json, but https://github.com/angular/angular-cli/issues/7347
32+
'--extract-css'
33+
]);
34+
devServerProc.stdout.pipe(process.stdout);
35+
36+
// Wait until the CLI dev server is listening before letting ASP.NET start the app
37+
console.log('Waiting for @angular/cli service to start...');
38+
waitForLine(devServerProc.stdout, /open your browser on (http\S+)/).then(function (matches) {
39+
var devServerUrl = url.parse(matches[1]);
40+
console.log('@angular/cli service has started on internal port ' + devServerUrl.port);
41+
callback(null, {
42+
Port: parseInt(devServerUrl.port)
43+
});
44+
});
45+
});
46+
}
47+
};
48+
49+
function waitForLine(stream, regex) {
50+
return new Promise(function (resolve, reject) {
51+
var lineReader = readline.createInterface({ input: stream });
52+
var listener = function (line) {
53+
var matches = regex.exec(line);
54+
if (matches) {
55+
lineReader.removeListener('line', listener);
56+
resolve(matches);
57+
}
58+
};
59+
lineReader.addListener('line', listener);
60+
});
61+
}
62+
63+
function executeAngularCli(args) {
64+
var angularCliBin = require.resolve('@angular/cli/bin/ng');
65+
return childProcess.fork(angularCliBin, args, {
66+
stdio: [/* stdin */ 'ignore', /* stdout */ 'pipe', /* stderr */ 'inherit', 'ipc']
67+
});
68+
}
69+
70+
function getOSAssignedPortNumber() {
71+
return new Promise(function (resolve, reject) {
72+
var server = net.createServer();
73+
server.listen(0, 'localhost', function () {
74+
var portNumber = server.address().port;
75+
server.close(function () { resolve(portNumber); });
76+
});
77+
});
78+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
<EmbeddedResource Include="Content\**\*" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" />
18+
</ItemGroup>
19+
20+
</Project>
Lines 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+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNetCore.SpaServices.Prerendering
8+
{
9+
/// <summary>
10+
/// Represents the ability to build a Single Page Application application on demand
11+
/// so that it can be prerendered. This is only intended to be used at development
12+
/// time. In production, a SPA should already be built during publishing.
13+
/// </summary>
14+
public interface ISpaPrerendererBuilder
15+
{
16+
/// <summary>
17+
/// Builds the Single Page Application so that a JavaScript entrypoint file
18+
/// exists on disk. Prerendering middleware can then execute that file in
19+
/// a Node environment.
20+
/// </summary>
21+
/// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
22+
/// <returns>A <see cref="Task"/> representing completion of the build process.</returns>
23+
Task Build(IApplicationBuilder appBuilder);
24+
}
25+
}

0 commit comments

Comments
 (0)