X Tutup
Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.
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,47 @@
// 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.AspNetCore.SpaServices.Prerendering;
using System;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.SpaServices.AngularCli
{
/// <summary>
/// Provides an implementation of <see cref="ISpaPrerendererBuilder"/> that can build
/// an Angular application by invoking the Angular CLI.
/// </summary>
public class AngularCliBuilder : ISpaPrerendererBuilder
{
private readonly string _cliAppName;

/// <summary>
/// Constructs an instance of <see cref="AngularCliBuilder"/>.
/// </summary>
/// <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>
public AngularCliBuilder(string cliAppName)
{
_cliAppName = cliAppName;
}

/// <inheritdoc />
public Task Build(IApplicationBuilder app)
{
// Locate the AngularCliMiddleware within the provided IApplicationBuilder
if (app.Properties.TryGetValue(
AngularCliMiddleware.AngularCliMiddlewareKey,
out var angularCliMiddleware))
{
return ((AngularCliMiddleware)angularCliMiddleware)
.StartAngularCliBuilderAsync(_cliAppName);
}
else
{
throw new Exception(
$"Cannot use {nameof(AngularCliBuilder)} unless you are also using" +
$" {nameof(AngularCliMiddlewareExtensions.UseAngularCliServer)}.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// 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 System;
using System.IO;
using Microsoft.AspNetCore.NodeServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using System.Threading;
using Microsoft.AspNetCore.SpaServices.Proxy;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.SpaServices.AngularCli
{
internal class AngularCliMiddleware
{
private const string _middlewareResourceName = "/Content/Node/angular-cli-middleware.js";

internal readonly static string AngularCliMiddlewareKey = Guid.NewGuid().ToString();

private readonly INodeServices _nodeServices;
private readonly string _middlewareScriptPath;

public AngularCliMiddleware(IApplicationBuilder appBuilder, string sourcePath, SpaDefaultPageMiddleware defaultPageMiddleware)
{
if (string.IsNullOrEmpty(sourcePath))
{
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
}

// Prepare to make calls into Node
_nodeServices = CreateNodeServicesInstance(appBuilder, sourcePath);
_middlewareScriptPath = GetAngularCliMiddlewareScriptPath(appBuilder);

// Start Angular CLI and attach to middleware pipeline
var angularCliServerInfoTask = StartAngularCliServerAsync();

// Proxy the corresponding requests through ASP.NET and into the Node listener
// Anything under /<publicpath> (e.g., /dist) is proxied as a normal HTTP request
// with a typical timeout (100s is the default from HttpClient).
UseProxyToLocalAngularCliMiddleware(appBuilder, defaultPageMiddleware,
angularCliServerInfoTask, TimeSpan.FromSeconds(100));

// Advertise the availability of this feature to other SPA middleware
appBuilder.Properties.Add(AngularCliMiddlewareKey, this);
}

public Task StartAngularCliBuilderAsync(string cliAppName)
{
return _nodeServices.InvokeExportAsync<AngularCliServerInfo>(
_middlewareScriptPath,
"startAngularCliBuilder",
cliAppName);
}

private static INodeServices CreateNodeServicesInstance(
IApplicationBuilder appBuilder, string sourcePath)
{
// Unlike other consumers of NodeServices, AngularCliMiddleware dosen't share Node instances, nor does it
// use your DI configuration. It's important for AngularCliMiddleware to have its own private Node instance
// because it must *not* restart when files change (it's designed to watch for changes and rebuild).
var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices)
{
WatchFileExtensions = new string[] { }, // Don't watch anything
ProjectPath = Path.Combine(Directory.GetCurrentDirectory(), sourcePath),
};

if (!Directory.Exists(nodeServicesOptions.ProjectPath))
{
throw new DirectoryNotFoundException($"Directory not found: {nodeServicesOptions.ProjectPath}");
}

return NodeServicesFactory.CreateNodeServices(nodeServicesOptions);
}

private static string GetAngularCliMiddlewareScriptPath(IApplicationBuilder appBuilder)
{
var script = EmbeddedResourceReader.Read(typeof(AngularCliMiddleware), _middlewareResourceName);
var nodeScript = new StringAsTempFile(script, GetStoppingToken(appBuilder));
return nodeScript.FileName;
}

private static CancellationToken GetStoppingToken(IApplicationBuilder appBuilder)
{
var applicationLifetime = appBuilder
.ApplicationServices
.GetService(typeof(IApplicationLifetime));
return ((IApplicationLifetime)applicationLifetime).ApplicationStopping;
}

private async Task<AngularCliServerInfo> StartAngularCliServerAsync()
{
// Tell Node to start the server hosting the Angular CLI
var angularCliServerInfo = await _nodeServices.InvokeExportAsync<AngularCliServerInfo>(
_middlewareScriptPath,
"startAngularCliServer");

// Even after the Angular CLI claims to be listening for requests, there's a short
// period where it will give an error if you make a request too quickly. Give it
// a moment to finish starting up.
await Task.Delay(500);

return angularCliServerInfo;
}

private static void UseProxyToLocalAngularCliMiddleware(
IApplicationBuilder appBuilder, SpaDefaultPageMiddleware defaultPageMiddleware,
Task<AngularCliServerInfo> serverInfoTask, TimeSpan requestTimeout)
{
// This is hardcoded to use http://localhost because:
// - the requests are always from the local machine (we're not accepting remote
// requests that go directly to the Angular CLI middleware server)
// - given that, there's no reason to use https, and we couldn't even if we
// wanted to, because in general the Angular CLI server has no certificate
var proxyOptionsTask = serverInfoTask.ContinueWith(
task => new ConditionalProxyMiddlewareTarget(
"http", "localhost", task.Result.Port.ToString()));

// Requests outside /<urlPrefix> are proxied to the default page
var hasRewrittenUrlMarker = new object();
var defaultPageUrl = defaultPageMiddleware.DefaultPageUrl;
var urlPrefix = defaultPageMiddleware.UrlPrefix;
var urlPrefixIsRoot = string.IsNullOrEmpty(urlPrefix) || urlPrefix == "/";
appBuilder.Use((context, next) =>
{
if (!urlPrefixIsRoot && !context.Request.Path.StartsWithSegments(urlPrefix))
{
context.Items[hasRewrittenUrlMarker] = context.Request.Path;
context.Request.Path = defaultPageUrl;
}

return next();
});

appBuilder.UseMiddleware<ConditionalProxyMiddleware>(urlPrefix, requestTimeout, proxyOptionsTask);

// If we rewrote the path, rewrite it back. Don't want to interfere with
// any other middleware.
appBuilder.Use((context, next) =>
{
if (context.Items.ContainsKey(hasRewrittenUrlMarker))
{
context.Request.Path = (PathString)context.Items[hasRewrittenUrlMarker];
context.Items.Remove(hasRewrittenUrlMarker);
}

return next();
});
}

#pragma warning disable CS0649
class AngularCliServerInfo
{
public int Port { get; set; }
}
}
#pragma warning restore CS0649
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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.AngularCli
{
/// <summary>
/// Extension methods for enabling Angular CLI middleware support.
/// </summary>
public static class AngularCliMiddlewareExtensions
{
/// <summary>
/// Handles requests by passing them through to an instance of the Angular CLI server.
/// This means you can always serve up-to-date CLI-built resources without having
/// to run the Angular CLI server manually.
///
/// This feature should only be used in development. For production deployments, be
/// sure not to enable the Angular CLI server.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <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>
public static void UseAngularCliServer(
this IApplicationBuilder app,
string sourcePath)
{
var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(app);
if (defaultPageMiddleware == null)
{
throw new Exception($"{nameof(UseAngularCliServer)} should be called inside the 'configue' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
}

new AngularCliMiddleware(app, sourcePath, defaultPageMiddleware);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.

var childProcess = require('child_process');
var net = require('net');
var readline = require('readline');
var url = require('url');

module.exports = {
startAngularCliBuilder: function startAngularCliBuilder(callback, appName) {
var proc = executeAngularCli([
'build',
'-app', appName,
'--watch'
]);
proc.stdout.pipe(process.stdout);
waitForLine(proc.stdout, /chunk/).then(function () {
callback();
});
},

startAngularCliServer: function startAngularCliServer(callback, options) {
getOSAssignedPortNumber().then(function (portNumber) {
// Start @angular/cli dev server on private port, and pipe its output
// back to the ASP.NET host process.
// TODO: Support streaming arbitrary chunks to host process's stdout
// rather than just full lines, so we can see progress being logged
var devServerProc = executeAngularCli([
'serve',
'--port', portNumber.toString(),
'--deploy-url', '/dist/', // Value should come from .angular-cli.json, but https://github.com/angular/angular-cli/issues/7347
'--extract-css'
]);
devServerProc.stdout.pipe(process.stdout);

// Wait until the CLI dev server is listening before letting ASP.NET start the app
console.log('Waiting for @angular/cli service to start...');
waitForLine(devServerProc.stdout, /open your browser on (http\S+)/).then(function (matches) {
var devServerUrl = url.parse(matches[1]);
console.log('@angular/cli service has started on internal port ' + devServerUrl.port);
callback(null, {
Port: parseInt(devServerUrl.port)
});
});
});
}
};

function waitForLine(stream, regex) {
return new Promise(function (resolve, reject) {
var lineReader = readline.createInterface({ input: stream });
lineReader.on('line', function (line) {
var matches = regex.exec(line);
if (matches) {
lineReader.close();
resolve(matches);
}
});
});
}

function executeAngularCli(args) {
var angularCliBin = require.resolve('@angular/cli/bin/ng');
return childProcess.fork(angularCliBin, args, {
stdio: [/* stdin */ 'ignore', /* stdout */ 'pipe', /* stderr */ 'inherit', 'ipc']
});
}

function getOSAssignedPortNumber() {
return new Promise(function (resolve, reject) {
var server = net.createServer();
server.listen(0, 'localhost', function () {
var portNumber = server.address().port;
server.close(function () { resolve(portNumber); });
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" />
</ItemGroup>

<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.NodeServices;
using System.Threading.Tasks;
using System;

namespace Microsoft.AspNetCore.SpaServices.Prerendering
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.Threading.Tasks;

namespace Microsoft.AspNetCore.SpaServices.Prerendering
{
/// <summary>
/// Represents the ability to build a Single Page Application application on demand
/// so that it can be prerendered. This is only intended to be used at development
/// time. In production, a SPA should already be built during publishing.
/// </summary>
public interface ISpaPrerendererBuilder
{
/// <summary>
/// Builds the Single Page Application so that a JavaScript entrypoint file
/// exists on disk. Prerendering middleware can then execute that file in
/// a Node environment.
/// </summary>
/// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
/// <returns>A <see cref="Task"/> representing completion of the build process.</returns>
Task Build(IApplicationBuilder appBuilder);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.NodeServices;
using Microsoft.AspNetCore.SpaServices.Prerendering;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.SpaServices.Prerendering;

namespace Microsoft.Extensions.DependencyInjection
{
Expand All @@ -20,7 +14,7 @@ public static class PrerenderingServiceCollectionExtensions
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
serviceCollection.AddHttpContextAccessor();
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
}
}
Expand Down
Loading
X Tutup