From 2241c55a9000c83592ea02dab54c6bc4359caacf Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 27 Sep 2016 15:45:21 +0100 Subject: [PATCH] Support for array-style webpack configs in aspnet-webpack. Fixes #291. --- .../Webpack/WebpackDevMiddleware.cs | 17 +- .../npm/aspnet-webpack/package.json | 2 +- .../src/WebpackDevMiddleware.ts | 154 ++++++++++-------- 3 files changed, 105 insertions(+), 68 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index e6a65cb..6c69e54 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -58,12 +58,20 @@ namespace Microsoft.AspNetCore.Builder var devServerOptions = new { webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile), - suppliedOptions = options + suppliedOptions = options, + understandsMultiplePublicPaths = true }; var devServerInfo = nodeServices.InvokeExportAsync(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; + // Older versions of aspnet-webpack just returned a single 'publicPath', but now we support multiple + if (devServerInfo.PublicPaths == null) + { + throw new InvalidOperationException( + "To enable Webpack dev middleware, you must update to a newer version of the aspnet-webpack NPM package."); + } + // Proxy the corresponding requests through ASP.NET and into the Node listener // 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 @@ -73,7 +81,10 @@ namespace Microsoft.AspNetCore.Builder // able to make outbound requests to it from here. var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, "localhost", devServerInfo.Port.ToString()); - appBuilder.UseMiddleware(devServerInfo.PublicPath, proxyOptions); + foreach (var publicPath in devServerInfo.PublicPaths) + { + appBuilder.UseMiddleware(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 @@ -95,7 +106,7 @@ namespace Microsoft.AspNetCore.Builder class WebpackDevServerInfo { public int Port { get; set; } - public string PublicPath { get; set; } + public string[] PublicPaths { get; set; } } } #pragma warning restore CS0649 diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index ae41bc1..6f47252 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-webpack", - "version": "1.0.14", + "version": "1.0.15", "description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts index e18f686..5aae453 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts @@ -4,11 +4,12 @@ import * as url from 'url'; import { requireNewCopy } from './RequireNewCopy'; export interface CreateDevServerCallback { - (error: any, result: { Port: number, PublicPath: string }): void; + (error: any, result: { Port: number, PublicPaths: string[] }): void; } // These are the options passed by WebpackDevMiddleware.cs interface CreateDevServerOptions { + understandsMultiplePublicPaths: boolean; // For checking that the NuGet package is recent enough. Can be removed when we no longer need back-compatibility. webpackConfigPath: string; suppliedOptions: DevServerOptions; } @@ -20,11 +21,83 @@ interface DevServerOptions { ReactHotModuleReplacement: boolean; } +function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean) { + // Build the final Webpack config based on supplied options + if (enableHotModuleReplacement) { + // For this, we only support the key/value config format, not string or string[], since + // those ones don't clearly indicate what the resulting bundle name will be + const entryPoints = webpackConfig.entry; + const isObjectStyleConfig = entryPoints + && typeof entryPoints === 'object' + && !(entryPoints instanceof Array); + if (!isObjectStyleConfig) { + throw new Error('To use HotModuleReplacement, your webpack config must specify an \'entry\' value as a key-value object (e.g., "entry: { main: \'ClientApp/boot-client.ts\' }")'); + } + + // Augment all entry points so they support HMR + Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => { + if (typeof entryPoints[entryPointName] === 'string') { + entryPoints[entryPointName] = ['webpack-hot-middleware/client', entryPoints[entryPointName]]; + } else { + entryPoints[entryPointName].unshift('webpack-hot-middleware/client'); + } + }); + + webpackConfig.plugins = webpackConfig.plugins || []; + webpackConfig.plugins.push( + new webpack.HotModuleReplacementPlugin() + ); + + // Set up React HMR support if requested. This requires the 'aspnet-webpack-react' package. + if (enableReactHotModuleReplacement) { + let aspNetWebpackReactModule: any; + try { + aspNetWebpackReactModule = require('aspnet-webpack-react'); + } catch(ex) { + throw new Error('ReactHotModuleReplacement failed because of an error while loading \'aspnet-webpack-react\'. Error was: ' + ex.stack); + } + + aspNetWebpackReactModule.addReactHotModuleReplacementBabelTransform(webpackConfig); + } + } + + // Attach Webpack dev middleware and optional 'hot' middleware + const compiler = webpack(webpackConfig); + app.use(require('webpack-dev-middleware')(compiler, { + noInfo: true, + publicPath: webpackConfig.output.publicPath + })); + + if (enableHotModuleReplacement) { + let webpackHotMiddlewareModule; + try { + webpackHotMiddlewareModule = require('webpack-hot-middleware'); + } catch (ex) { + throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack); + } + app.use(webpackHotMiddlewareModule(compiler)); + } +} + export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) { const options: CreateDevServerOptions = JSON.parse(optionsJson); - const webpackConfig: webpack.Configuration = requireNewCopy(options.webpackConfigPath); - const publicPath = (webpackConfig.output.publicPath || '').trim(); - if (!publicPath) { + + if (!options.understandsMultiplePublicPaths) { + callback('To use Webpack dev server, you must update to a newer version of the Microsoft.AspNetCore.SpaServices package', null); + return; + } + + // Read the webpack config's export, and normalize it into the more general 'array of configs' format + let webpackConfigArray: webpack.Configuration[] = requireNewCopy(options.webpackConfigPath); + if (!(webpackConfigArray instanceof Array)) { + webpackConfigArray = [webpackConfigArray as webpack.Configuration]; + } + + // Check that at least one of the configurations specifies a publicPath. Those are the only ones we'll + // enable middleware for, and if there aren't any, you must be making a mistake. + const webpackConfigsWithPublicPath = webpackConfigArray + .filter(webpackConfig => (webpackConfig.output.publicPath || '').trim() !== ''); + if (webpackConfigsWithPublicPath.length === 0) { callback('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack.config.', null); return; } @@ -41,69 +114,22 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option const app = connect(); const listener = app.listen(suggestedHMRPortOrZero, () => { - // Build the final Webpack config based on supplied options - if (enableHotModuleReplacement) { - // For this, we only support the key/value config format, not string or string[], since - // those ones don't clearly indicate what the resulting bundle name will be - const entryPoints = webpackConfig.entry; - const isObjectStyleConfig = entryPoints - && typeof entryPoints === 'object' - && !(entryPoints instanceof Array); - if (!isObjectStyleConfig) { - callback('To use HotModuleReplacement, your webpack config must specify an \'entry\' value as a key-value object (e.g., "entry: { main: \'ClientApp/boot-client.ts\' }")', null); - return; - } - - // Augment all entry points so they support HMR - Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => { - if (typeof entryPoints[entryPointName] === 'string') { - entryPoints[entryPointName] = ['webpack-hot-middleware/client', entryPoints[entryPointName]]; - } else { - entryPoints[entryPointName].unshift('webpack-hot-middleware/client'); - } + try { + // For each webpack config that specifies a public path, add webpack dev middleware for it + webpackConfigsWithPublicPath.forEach(webpackConfig => { + attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement); }); - webpackConfig.plugins.push( - new webpack.HotModuleReplacementPlugin() - ); - - // Set up React HMR support if requested. This requires the 'aspnet-webpack-react' package. - if (enableReactHotModuleReplacement) { - let aspNetWebpackReactModule: any; - try { - aspNetWebpackReactModule = require('aspnet-webpack-react'); - } catch(ex) { - callback('ReactHotModuleReplacement failed because of an error while loading \'aspnet-webpack-react\'. Error was: ' + ex.stack, null); - return; - } - - aspNetWebpackReactModule.addReactHotModuleReplacementBabelTransform(webpackConfig); - } + // Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here + callback(null, { + Port: listener.address().port, + PublicPaths: webpackConfigsWithPublicPath.map(webpackConfig => + removeTrailingSlash(getPath(webpackConfig.output.publicPath)) + ) + }); + } catch (ex) { + callback(ex.stack, null); } - - // Attach Webpack dev middleware and optional 'hot' middleware - const compiler = webpack(webpackConfig); - app.use(require('webpack-dev-middleware')(compiler, { - noInfo: true, - publicPath: publicPath - })); - - if (enableHotModuleReplacement) { - let webpackHotMiddlewareModule; - try { - webpackHotMiddlewareModule = require('webpack-hot-middleware'); - } catch (ex) { - callback('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack, null); - return; - } - app.use(webpackHotMiddlewareModule(compiler)); - } - - // Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here - callback(null, { - Port: listener.address().port, - PublicPath: removeTrailingSlash(getPath(publicPath)) - }); }); }