mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 01:58:29 +00:00
For HMR, proxy all requests including /__webpack_hmr. Fixes #271.
This commit is contained in:
@@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
_pathPrefix = pathPrefix;
|
_pathPrefix = pathPrefix;
|
||||||
_options = options;
|
_options = options;
|
||||||
_httpClient = new HttpClient(new HttpClientHandler());
|
_httpClient = new HttpClient(new HttpClientHandler());
|
||||||
|
_httpClient.Timeout = _options.RequestTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SpaServices.Webpack
|
namespace Microsoft.AspNetCore.SpaServices.Webpack
|
||||||
{
|
{
|
||||||
internal class ConditionalProxyMiddlewareOptions
|
internal class ConditionalProxyMiddlewareOptions
|
||||||
{
|
{
|
||||||
public ConditionalProxyMiddlewareOptions(string scheme, string host, string port)
|
public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout)
|
||||||
{
|
{
|
||||||
Scheme = scheme;
|
Scheme = scheme;
|
||||||
Host = host;
|
Host = host;
|
||||||
Port = port;
|
Port = port;
|
||||||
|
RequestTimeout = requestTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Scheme { get; }
|
public string Scheme { get; }
|
||||||
public string Host { get; }
|
public string Host { get; }
|
||||||
public string Port { get; }
|
public string Port { get; }
|
||||||
|
public TimeSpan RequestTimeout { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
using Microsoft.AspNetCore.SpaServices.Webpack;
|
||||||
@@ -7,6 +8,7 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.PlatformAbstractions;
|
using Microsoft.Extensions.PlatformAbstractions;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
{
|
{
|
||||||
@@ -15,8 +17,6 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WebpackDevMiddleware
|
public static class WebpackDevMiddleware
|
||||||
{
|
{
|
||||||
private const string WebpackDevMiddlewareScheme = "http";
|
|
||||||
private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr";
|
|
||||||
private const string DefaultConfigFile = "webpack.config.js";
|
private const string DefaultConfigFile = "webpack.config.js";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -75,12 +75,18 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
"/Content/Node/webpack-dev-middleware.js");
|
"/Content/Node/webpack-dev-middleware.js");
|
||||||
var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
|
var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
|
||||||
|
|
||||||
|
// Ideally, this would be relative to the application's PathBase (so it could work in virtual directories)
|
||||||
|
// but it's not clear that such information exists during application startup, as opposed to within the context
|
||||||
|
// of a request.
|
||||||
|
var hmrEndpoint = "/__webpack_hmr";
|
||||||
|
|
||||||
// Tell Node to start the server hosting webpack-dev-middleware
|
// Tell Node to start the server hosting webpack-dev-middleware
|
||||||
var devServerOptions = new
|
var devServerOptions = new
|
||||||
{
|
{
|
||||||
webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile),
|
webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile),
|
||||||
suppliedOptions = options,
|
suppliedOptions = options,
|
||||||
understandsMultiplePublicPaths = true
|
understandsMultiplePublicPaths = true,
|
||||||
|
hotModuleReplacementEndpointUrl = hmrEndpoint
|
||||||
};
|
};
|
||||||
var devServerInfo =
|
var devServerInfo =
|
||||||
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
||||||
@@ -94,33 +100,30 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proxy the corresponding requests through ASP.NET and into the Node listener
|
// 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),
|
||||||
|
// plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request).
|
||||||
|
foreach (var publicPath in devServerInfo.PublicPaths)
|
||||||
|
{
|
||||||
|
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100));
|
||||||
|
}
|
||||||
|
appBuilder.UseProxyToLocalWebpackDevMiddleware(hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout)
|
||||||
|
{
|
||||||
// Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the
|
// Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the
|
||||||
// server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is
|
// server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is
|
||||||
// the one making the internal HTTP requests, and it's going to be to some port on this machine
|
// the one making the internal HTTP requests, and it's going to be to some port on this machine
|
||||||
// because aspnet-webpack hosts the dev server there. We can't use the hostname that the client
|
// because aspnet-webpack hosts the dev server there. We can't use the hostname that the client
|
||||||
// sees, because that could be anything (e.g., some upstream load balancer) and we might not be
|
// sees, because that could be anything (e.g., some upstream load balancer) and we might not be
|
||||||
// able to make outbound requests to it from here.
|
// able to make outbound requests to it from here.
|
||||||
var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme,
|
// Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS,
|
||||||
"localhost", devServerInfo.Port.ToString());
|
// because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic
|
||||||
foreach (var publicPath in devServerInfo.PublicPaths)
|
// to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have
|
||||||
{
|
// the necessary certificate).
|
||||||
appBuilder.UseMiddleware<ConditionalProxyMiddleware>(publicPath, proxyOptions);
|
var proxyOptions = new ConditionalProxyMiddlewareOptions(
|
||||||
}
|
"http", "localhost", proxyToPort.ToString(), requestTimeout);
|
||||||
|
appBuilder.UseMiddleware<ConditionalProxyMiddleware>(publicPath, proxyOptions);
|
||||||
// While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream,
|
|
||||||
// and the Microsoft.AspNetCore.Proxy code doesn't handle that entirely - it throws an exception after
|
|
||||||
// a while. So, just serve a 302 for those. But note that we must use the hostname that the client
|
|
||||||
// sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker).
|
|
||||||
appBuilder.Map(WebpackHotMiddlewareEndpoint, builder =>
|
|
||||||
{
|
|
||||||
builder.Use(next => ctx =>
|
|
||||||
{
|
|
||||||
var hostname = ctx.Request.Host.Host;
|
|
||||||
ctx.Response.Redirect(
|
|
||||||
$"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}");
|
|
||||||
return Task.FromResult(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS0649
|
#pragma warning disable CS0649
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface CreateDevServerCallback {
|
|||||||
interface CreateDevServerOptions {
|
interface CreateDevServerOptions {
|
||||||
webpackConfigPath: string;
|
webpackConfigPath: string;
|
||||||
suppliedOptions: DevServerOptions;
|
suppliedOptions: DevServerOptions;
|
||||||
|
hotModuleReplacementEndpointUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are the options configured in C# and then JSON-serialized, hence the C#-style naming
|
// These are the options configured in C# and then JSON-serialized, hence the C#-style naming
|
||||||
@@ -28,7 +29,7 @@ interface DevServerOptions {
|
|||||||
ReactHotModuleReplacement: boolean;
|
ReactHotModuleReplacement: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrEndpoint: string) {
|
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrClientEndpoint: string, hmrServerEndpoint: string) {
|
||||||
// Build the final Webpack config based on supplied options
|
// Build the final Webpack config based on supplied options
|
||||||
if (enableHotModuleReplacement) {
|
if (enableHotModuleReplacement) {
|
||||||
// For this, we only support the key/value config format, not string or string[], since
|
// For this, we only support the key/value config format, not string or string[], since
|
||||||
@@ -44,7 +45,7 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
|
|||||||
// Augment all entry points so they support HMR (unless they already do)
|
// Augment all entry points so they support HMR (unless they already do)
|
||||||
Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => {
|
Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => {
|
||||||
const webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client';
|
const webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client';
|
||||||
const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrEndpoint);
|
const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrClientEndpoint);
|
||||||
if (typeof entryPoints[entryPointName] === 'string') {
|
if (typeof entryPoints[entryPointName] === 'string') {
|
||||||
entryPoints[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints[entryPointName]];
|
entryPoints[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints[entryPointName]];
|
||||||
} else if (firstIndexOfStringStartingWith(entryPoints[entryPointName], webpackHotMiddlewareEntryPoint) < 0) {
|
} else if (firstIndexOfStringStartingWith(entryPoints[entryPointName], webpackHotMiddlewareEntryPoint) < 0) {
|
||||||
@@ -117,7 +118,9 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack);
|
throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack);
|
||||||
}
|
}
|
||||||
app.use(webpackHotMiddlewareModule(compiler));
|
app.use(webpackHotMiddlewareModule(compiler, {
|
||||||
|
path: hmrServerEndpoint
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +201,16 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
|
|||||||
}
|
}
|
||||||
normalizedPublicPaths.push(removeTrailingSlash(publicPath));
|
normalizedPublicPaths.push(removeTrailingSlash(publicPath));
|
||||||
|
|
||||||
const hmrEndpoint = `http://localhost:${listener.address().port}/__webpack_hmr`;
|
// Newer versions of Microsoft.AspNetCore.SpaServices will explicitly pass an HMR endpoint URL
|
||||||
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrEndpoint);
|
// (because it's relative to the app's URL space root, which the client doesn't otherwise know).
|
||||||
|
// For back-compatibility, fall back on connecting directly to the underlying HMR server (though
|
||||||
|
// that won't work if the app is hosted on HTTPS because of the mixed-content rule, and we can't
|
||||||
|
// run the HMR server itself on HTTPS because in general it has no valid cert).
|
||||||
|
const hmrClientEndpoint = options.hotModuleReplacementEndpointUrl // The URL that we'll proxy (e.g., /__asp_webpack_hmr)
|
||||||
|
|| `http://localhost:${listener.address().port}/__webpack_hmr`; // Fall back on absolute URL to bypass proxying
|
||||||
|
const hmrServerEndpoint = options.hotModuleReplacementEndpointUrl
|
||||||
|
|| '/__webpack_hmr'; // URL is relative to webpack dev server root
|
||||||
|
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientEndpoint, hmrServerEndpoint);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user