diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 244bbfe..1aa8595 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -1,5 +1,6 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.SpaServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -51,6 +52,11 @@ namespace ReactExample // send the request to the following path or controller action. app.UseExceptionHandler("/Home/Error"); } + + app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { + HotModuleReplacement = true, + ReactHotModuleReplacement = true + }); // Add static files to the request pipeline. app.UseStaticFiles(); diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json index 5fc1c48..826c5a5 100644 --- a/samples/react/ReactGrid/package.json +++ b/samples/react/ReactGrid/package.json @@ -15,13 +15,18 @@ }, "devDependencies": { "babel-loader": "^6.2.1", + "babel-plugin-react-transform": "^2.0.0", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "css-loader": "^0.21.0", + "express": "^4.13.4", "extract-text-webpack-plugin": "^0.8.2", "file-loader": "^0.8.4", + "react-transform-hmr": "^1.0.1", "style-loader": "^0.13.0", "url-loader": "^0.5.6", - "webpack": "^1.12.2" + "webpack": "^1.12.2", + "webpack-dev-middleware": "^1.5.1", + "webpack-hot-middleware": "^2.6.4" } } diff --git a/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js b/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js new file mode 100644 index 0000000..3314b14 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js @@ -0,0 +1,92 @@ +var express = require('express'); +var webpack = require('webpack'); +var defaultPort = 0; // 0 means 'choose randomly'. Could allow an explicit value to be supplied instead. + +module.exports = { + createWebpackDevServer: function(callback, optionsJson) { + var options = JSON.parse(optionsJson); + var webpackConfig = require(options.webpackConfigPath); + var publicPath = (webpackConfig.output.publicPath || '').trim(); + if (!publicPath) { + throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack.config.'); + } + + var enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement; + var enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement; + + var app = new express(); + var listener = app.listen(defaultPort, function() { + // Build the final Webpack config based on supplied options + if (enableHotModuleReplacement) { + webpackConfig.entry.main.unshift('webpack-hot-middleware/client'); + webpackConfig.plugins.push( + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() + ); + + if (enableReactHotModuleReplacement) { + addReactHotModuleReplacementBabelTransform(webpackConfig); + } + } + + // Attach Webpack dev middleware and optional 'hot' middleware + var compiler = webpack(webpackConfig); + app.use(require('webpack-dev-middleware')(compiler, { + noInfo: true, + publicPath: publicPath + })); + + if (enableHotModuleReplacement) { + app.use(require('webpack-hot-middleware')(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(publicPath) + }); + }); + } +}; + +function addReactHotModuleReplacementBabelTransform(webpackConfig) { + webpackConfig.module.loaders.forEach(function(loaderConfig) { + if (loaderConfig.loader && loaderConfig.loader.match(/\bbabel-loader\b/)) { + // Ensure the babel-loader options includes a 'query' + var query = loaderConfig.query = loaderConfig.query || {}; + + // Ensure Babel plugins includes 'react-transform' + var plugins = query.plugins = query.plugins || []; + if (!plugins.some(function(pluginConfig) { + return pluginConfig && pluginConfig[0] === 'react-transform'; + })) { + plugins.push(['react-transform', {}]); + } + + // Ensure 'react-transform' plugin is configured to use 'react-transform-hmr' + plugins.forEach(function(pluginConfig) { + if (pluginConfig && pluginConfig[0] === 'react-transform') { + var pluginOpts = pluginConfig[1] = pluginConfig[1] || {}; + var transforms = pluginOpts.transforms = pluginOpts.transforms || []; + if (!transforms.some(function(transform) { + return transform.transform === 'react-transform-hmr'; + })) { + transforms.push({ + transform: "react-transform-hmr", + imports: ["react"], + locals: ["module"] // Important for Webpack HMR + }); + } + } + }); + } + }); +} + +function removeTrailingSlash(str) { + if (str.lastIndexOf('/') === str.length - 1) { + str = str.substring(0, str.length - 1); + } + + return str; +} diff --git a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs b/src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs new file mode 100644 index 0000000..56818c6 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using Microsoft.AspNet.NodeServices; +using Microsoft.AspNet.Proxy; +using Microsoft.AspNet.SpaServices; +using Microsoft.Extensions.PlatformAbstractions; +using Newtonsoft.Json; + +// Putting in this namespace so it's always available whenever MapRoute is +namespace Microsoft.AspNet.Builder +{ + public static class WebpackDevMiddleware + { + const string WebpackDevMiddlewareHostname = "localhost"; + const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; + + static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI + + public static void UseWebpackDevMiddleware(this IApplicationBuilder appBuilder, WebpackDevMiddlewareOptions options = null) { + // Validate options + if (options != null) { + if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) { + throw new ArgumentException("To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); + } + } + + // Get the NodeServices instance from DI + var nodeServices = (INodeServices)appBuilder.ApplicationServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + var appEnv = (IApplicationEnvironment)appBuilder.ApplicationServices.GetService(typeof(IApplicationEnvironment)); + if (nodeServices == null) { + nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http, appEnv.ApplicationBasePath); + } + + // Get a filename matching the middleware Node script + var script = EmbeddedResourceReader.Read(typeof (WebpackDevMiddleware), "/Content/Node/webpack-dev-middleware.js"); + var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + + // Tell Node to start the server hosting webpack-dev-middleware + var devServerOptions = new { + webpackConfigPath = Path.Combine(appEnv.ApplicationBasePath, "webpack.config.js"), + suppliedOptions = options ?? new WebpackDevMiddlewareOptions() + }; + var devServerInfo = nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; + + // Proxy the corresponding requests through ASP.NET and into the Node listener + appBuilder.Map(devServerInfo.PublicPath, builder => { + builder.RunProxy(new ProxyOptions { + Host = WebpackDevMiddlewareHostname, + Port = devServerInfo.Port.ToString() + }); + }); + appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => { + builder.RunProxy(new ProxyOptions { + Host = WebpackDevMiddlewareHostname, + Port = devServerInfo.Port.ToString() + }); + }); + } + + class WebpackDevServerInfo { + public int Port; + public string PublicPath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs new file mode 100644 index 0000000..1fa5c56 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs @@ -0,0 +1,6 @@ +namespace Microsoft.AspNet.SpaServices { + public class WebpackDevMiddlewareOptions { + public bool HotModuleReplacement { get; set; } + public bool ReactHotModuleReplacement { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SpaServices/project.json b/src/Microsoft.AspNet.SpaServices/project.json index 19fc114..7c9b900 100644 --- a/src/Microsoft.AspNet.SpaServices/project.json +++ b/src/Microsoft.AspNet.SpaServices/project.json @@ -14,7 +14,9 @@ "licenseUrl": "", "dependencies": { "Microsoft.AspNet.Mvc": "6.0.0-rc1-final", - "Microsoft.AspNet.Routing": "1.0.0-rc1-final" + "Microsoft.AspNet.Routing": "1.0.0-rc1-final", + "Microsoft.AspNet.Proxy": "1.0.0-rc1-final", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha7" }, "frameworks": { "net451": { }, @@ -22,5 +24,8 @@ "dependencies": { } } - } + }, + "resource": [ + "Content/**/*" + ] }