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

Commit c47d5bf

Browse files
Add SPA middleware APIs to support new templates
1 parent e67a301 commit c47d5bf

15 files changed

+698
-34
lines changed
Lines 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+
usingMicrosoft.AspNetCore.SpaServices.Prerendering;
5+
usingSystem;
6+
usingSystem.Linq;
7+
usingSystem.Threading.Tasks;
8+
9+
namespaceMicrosoft.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+
publicclassAngularCliBuilder:ISpaPrerendererBuilder
16+
{
17+
privatereadonlystring_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+
publicAngularCliBuilder(stringcliAppName)
24+
{
25+
_cliAppName=cliAppName;
26+
}
27+
28+
/// <inheritdoc />
29+
publicTaskBuild(ISpaBuilderspaBuilder)
30+
{
31+
// Locate the AngularCliMiddleware within the provided ISpaBuilder
32+
varangularCliMiddleware=spaBuilder
33+
.Properties.Keys.OfType<AngularCliMiddleware>().FirstOrDefault();
34+
if(angularCliMiddleware==null)
35+
{
36+
thrownewException(
37+
$"Cannot use {nameof(AngularCliBuilder)} unless you are also using {nameof(AngularCliMiddleware)}.");
38+
}
39+
40+
returnangularCliMiddleware.StartAngularCliBuilderAsync(_cliAppName);
41+
}
42+
}
43+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
usingSystem;
5+
usingSystem.IO;
6+
usingMicrosoft.AspNetCore.NodeServices;
7+
usingSystem.Threading.Tasks;
8+
usingMicrosoft.AspNetCore.Builder;
9+
usingMicrosoft.AspNetCore.Hosting;
10+
usingSystem.Threading;
11+
usingMicrosoft.AspNetCore.SpaServices.Proxy;
12+
13+
namespaceMicrosoft.AspNetCore.SpaServices.AngularCli
14+
{
15+
internalclassAngularCliMiddleware
16+
{
17+
privateconststring_middlewareResourceName="/Content/Node/angular-cli-middleware.js";
18+
19+
privatereadonlyINodeServices_nodeServices;
20+
privatereadonlystring_middlewareScriptPath;
21+
22+
publicAngularCliMiddleware(ISpaBuilderspaBuilder,stringsourcePath)
23+
{
24+
if(string.IsNullOrEmpty(sourcePath))
25+
{
26+
thrownewArgumentException("Cannot be null or empty",nameof(sourcePath));
27+
}
28+
29+
// Prepare to make calls into Node
30+
varappBuilder=spaBuilder.AppBuilder;
31+
_nodeServices=CreateNodeServicesInstance(appBuilder,sourcePath);
32+
_middlewareScriptPath=GetAngularCliMiddlewareScriptPath(appBuilder);
33+
34+
// Start Angular CLI and attach to middleware pipeline
35+
varangularCliServerInfoTask=StartAngularCliServerAsync();
36+
spaBuilder.AddStartupTask(angularCliServerInfoTask);
37+
38+
// Proxy the corresponding requests through ASP.NET and into the Node listener
39+
// Anything under /<publicpath> (e.g., /dist) is proxied as a normal HTTP request
40+
// with a typical timeout (100s is the default from HttpClient).
41+
UseProxyToLocalAngularCliMiddleware(appBuilder,spaBuilder.PublicPath,
42+
angularCliServerInfoTask,TimeSpan.FromSeconds(100));
43+
44+
// Advertise the availability of this feature to other SPA middleware
45+
spaBuilder.Properties.Add(this,null);
46+
}
47+
48+
publicTaskStartAngularCliBuilderAsync(stringcliAppName)
49+
{
50+
return_nodeServices.InvokeExportAsync<AngularCliServerInfo>(
51+
_middlewareScriptPath,
52+
"startAngularCliBuilder",
53+
cliAppName);
54+
}
55+
56+
privatestaticINodeServicesCreateNodeServicesInstance(
57+
IApplicationBuilderappBuilder,stringsourcePath)
58+
{
59+
// Unlike other consumers of NodeServices, AngularCliMiddleware dosen't share Node instances, nor does it
60+
// use your DI configuration. It's important for AngularCliMiddleware to have its own private Node instance
61+
// because it must *not* restart when files change (it's designed to watch for changes and rebuild).
62+
varnodeServicesOptions=newNodeServicesOptions(appBuilder.ApplicationServices)
63+
{
64+
WatchFileExtensions=newstring[]{},// Don't watch anything
65+
ProjectPath=Path.Combine(Directory.GetCurrentDirectory(),sourcePath),
66+
};
67+
68+
returnNodeServicesFactory.CreateNodeServices(nodeServicesOptions);
69+
}
70+
71+
privatestaticstringGetAngularCliMiddlewareScriptPath(IApplicationBuilderappBuilder)
72+
{
73+
varscript=EmbeddedResourceReader.Read(typeof(AngularCliMiddleware),_middlewareResourceName);
74+
varnodeScript=newStringAsTempFile(script,GetStoppingToken(appBuilder));
75+
returnnodeScript.FileName;
76+
}
77+
78+
privatestaticCancellationTokenGetStoppingToken(IApplicationBuilderappBuilder)
79+
{
80+
varapplicationLifetime=appBuilder
81+
.ApplicationServices
82+
.GetService(typeof(IApplicationLifetime));
83+
return((IApplicationLifetime)applicationLifetime).ApplicationStopping;
84+
}
85+
86+
privateasyncTask<AngularCliServerInfo>StartAngularCliServerAsync()
87+
{
88+
// Tell Node to start the server hosting the Angular CLI
89+
varangularCliServerInfo=await_nodeServices.InvokeExportAsync<AngularCliServerInfo>(
90+
_middlewareScriptPath,
91+
"startAngularCliServer");
92+
93+
// Even after the Angular CLI claims to be listening for requests, there's a short
94+
// period where it will give an error if you make a request too quickly. Give it
95+
// a moment to finish starting up.
96+
awaitTask.Delay(500);
97+
98+
returnangularCliServerInfo;
99+
}
100+
101+
privatestaticvoidUseProxyToLocalAngularCliMiddleware(
102+
IApplicationBuilderappBuilder,stringpublicPath,
103+
Task<AngularCliServerInfo>serverInfoTask,TimeSpanrequestTimeout)
104+
{
105+
// This is hardcoded to use http://localhost because:
106+
// - the requests are always from the local machine (we're not accepting remote
107+
// requests that go directly to the Angular CLI middleware server)
108+
// - given that, there's no reason to use https, and we couldn't even if we
109+
// wanted to, because in general the Angular CLI server has no certificate
110+
varproxyOptionsTask=serverInfoTask.ContinueWith(
111+
task =>newConditionalProxyMiddlewareTarget(
112+
"http","localhost",task.Result.Port.ToString()));
113+
appBuilder.UseMiddleware<ConditionalProxyMiddleware>(publicPath,requestTimeout,proxyOptionsTask);
114+
}
115+
116+
#pragma warning disable CS0649
117+
classAngularCliServerInfo
118+
{
119+
publicintPort{get;set;}
120+
}
121+
}
122+
#pragma warning restore CS0649
123+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
namespaceMicrosoft.AspNetCore.SpaServices.AngularCli
5+
{
6+
/// <summary>
7+
/// Extension methods for enabling Angular CLI middleware support.
8+
/// </summary>
9+
publicstaticclassAngularCliMiddlewareExtensions
10+
{
11+
/// <summary>
12+
/// Enables Angular CLI middleware support. This hosts an instance of the Angular CLI in memory in
13+
/// your application so that you can always serve up-to-date CLI-built resources without having
14+
/// to run CLI server manually.
15+
///
16+
/// Incoming requests that match Angular CLI-built files will be handled by returning the CLI server
17+
/// output directly.
18+
///
19+
/// This feature should only be used in development. For production deployments, be sure not to
20+
/// enable Angular CLI middleware.
21+
/// </summary>
22+
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
23+
/// <param name="sourcePath">The path, relative to the application root, of the directory containing the SPA source files.</param>
24+
publicstaticvoidUseAngularCliMiddleware(
25+
thisISpaBuilderspaBuilder,
26+
stringsourcePath)
27+
{
28+
newAngularCliMiddleware(spaBuilder,sourcePath);
29+
}
30+
}
31+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
varchildProcess=require('child_process');
5+
varnet=require('net');
6+
varreadline=require('readline');
7+
varurl=require('url');
8+
9+
module.exports={
10+
startAngularCliBuilder: functionstartAngularCliBuilder(callback,appName){
11+
varproc=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: functionstartAngularCliServer(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+
vardevServerProc=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,/openyourbrowseron(http\S+)/).then(function(matches){
39+
vardevServerUrl=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+
functionwaitForLine(stream,regex){
50+
returnnewPromise(function(resolve,reject){
51+
varlineReader=readline.createInterface({input: stream});
52+
lineReader.on('line',function(line){
53+
varmatches=regex.exec(line);
54+
if(matches){
55+
lineReader.close();
56+
resolve(matches);
57+
}
58+
});
59+
});
60+
}
61+
62+
functionexecuteAngularCli(args){
63+
varangularCliBin=require.resolve('@angular/cli/bin/ng');
64+
returnchildProcess.fork(angularCliBin,args,{
65+
stdio: [/* stdin */'ignore',/* stdout */'pipe',/* stderr */'inherit','ipc']
66+
});
67+
}
68+
69+
functiongetOSAssignedPortNumber(){
70+
returnnewPromise(function(resolve,reject){
71+
varserver=net.createServer();
72+
server.listen(0,'localhost',function(){
73+
varportNumber=server.address().port;
74+
server.close(function(){resolve(portNumber);});
75+
});
76+
});
77+
}
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+
usingMicrosoft.AspNetCore.Builder;
5+
usingMicrosoft.AspNetCore.Http;
6+
usingSystem.Collections.Generic;
7+
usingSystem.Threading.Tasks;
8+
9+
namespaceMicrosoft.AspNetCore.SpaServices
10+
{
11+
internalclassDefaultSpaBuilder:ISpaBuilder
12+
{
13+
privatereadonlyobject_startupTasksLock=newobject();
14+
15+
publicDefaultSpaBuilder(IApplicationBuilderappBuilder,stringpublicPath,PathStringdefaultFilePath)
16+
{
17+
AppBuilder=appBuilder;
18+
DefaultFilePath=defaultFilePath;
19+
Properties=newDictionary<object,object>();
20+
PublicPath=publicPath;
21+
}
22+
23+
publicIApplicationBuilderAppBuilder{get;}
24+
publicPathStringDefaultFilePath{get;}
25+
publicIDictionary<object,object>Properties{get;}
26+
publicstringPublicPath{get;}
27+
publicTaskStartupTasks{get;privateset;}=Task.CompletedTask;
28+
29+
publicvoidAddStartupTask(Tasktask)
30+
{
31+
lock(_startupTasksLock)
32+
{
33+
StartupTasks=Task.WhenAll(StartupTasks,task);
34+
}
35+
}
36+
}
37+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
usingMicrosoft.AspNetCore.Builder;
5+
usingMicrosoft.AspNetCore.Http;
6+
usingSystem.Collections.Generic;
7+
usingSystem.Threading.Tasks;
8+
9+
namespaceMicrosoft.AspNetCore.SpaServices
10+
{
11+
/// <summary>
12+
/// Defines a class that provides mechanisms to configure a Single Page Application
13+
/// being hosted by an ASP.NET server.
14+
/// </summary>
15+
publicinterfaceISpaBuilder
16+
{
17+
/// <summary>
18+
/// Gets the <see cref="IApplicationBuilder"/> for the host application.
19+
/// </summary>
20+
IApplicationBuilderAppBuilder{get;}
21+
22+
/// <summary>
23+
/// Gets the path to the SPA's default file. By default, this is the file
24+
/// index.html within the <see cref="PublicPath"/>.
25+
/// </summary>
26+
PathStringDefaultFilePath{get;}
27+
28+
/// <summary>
29+
/// Gets the URL path, relative to the application's <c>PathBase</c>, from which
30+
/// the SPA files are served.
31+
///</summary>
32+
///<example>
33+
/// If the SPA files are located in <c>wwwroot/dist</c>, then the value would
34+
/// usually be <c>"dist"</c>, because that is the URL prefix from which clients
35+
/// can request those files.
36+
///</example>
37+
stringPublicPath{get;}
38+
39+
/// <summary>
40+
/// Gets a key/value collection that can be used to share data between SPA middleware.
41+
/// </summary>
42+
IDictionary<object,object>Properties{get;}
43+
44+
/// <summary>
45+
/// Gets a <see cref="Task"/> that represents the completion of all registered
46+
/// SPA startup tasks.
47+
/// </summary>
48+
TaskStartupTasks{get;}
49+
50+
/// <summary>
51+
/// Registers a task that represents part of SPA startup process. Middleware
52+
/// may choose to wait for these tasks to complete before taking some action.
53+
/// </summary>
54+
/// <param name="task">The <see cref="Task"/>.</param>
55+
voidAddStartupTask(Tasktask);
56+
}
57+
}

‎src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<PackageReferenceInclude="Microsoft.AspNetCore.Mvc.TagHelpers" />
1919
<PackageReferenceInclude="Microsoft.AspNetCore.Mvc.ViewFeatures" />
20+
<PackageReferenceInclude="Microsoft.AspNetCore.StaticFiles" />
2021
</ItemGroup>
2122

2223
<TargetName="PrepublishScript"BeforeTargets="PrepareForPublish"Condition=" '$(IsCrossTargetingBuild)' != 'true' ">

0 commit comments

Comments
(0)