From 6c903f33aeebf646ef8a9d9a3d19b0734acb7c5a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 9 Feb 2016 16:42:42 -0800 Subject: [PATCH] Move React server-side rendering into more general SpaServices package --- .../react/MusicStore/ReactApp/boot-server.tsx | 40 +++--- samples/react/MusicStore/ReactApp/boot.tsx | 4 +- .../MusicStore/ReactApp/fx/render-server.js | 54 ------- .../ReactApp/fx/require-ts-babel.js | 44 ------ samples/react/MusicStore/Startup.cs | 6 +- .../react/MusicStore/Views/Home/Index.cshtml | 2 +- .../MusicStore/Views/_ViewImports.cshtml | 1 + .../Content/Node/entrypoint-http.js | 2 +- .../ReactRenderer.cs | 24 ---- .../Content/Node/prerenderer.js | 135 ++++++++++++++++++ .../Prerendering/PrerenderTagHelper.cs} | 34 +++-- .../Prerendering/Prerenderer.cs | 32 +++++ .../{ => Routing}/SpaRouteConstraint.cs | 2 +- .../{ => Routing}/SpaRouteExtensions.cs | 0 .../{ => Webpack}/WebpackDevMiddleware.cs | 2 +- .../WebpackDevMiddlewareOptions.cs | 2 +- 16 files changed, 225 insertions(+), 159 deletions(-) delete mode 100644 samples/react/MusicStore/ReactApp/fx/render-server.js delete mode 100644 samples/react/MusicStore/ReactApp/fx/require-ts-babel.js delete mode 100644 src/Microsoft.AspNet.ReactServices/ReactRenderer.cs create mode 100644 src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js rename src/{Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs => Microsoft.AspNet.SpaServices/Prerendering/PrerenderTagHelper.cs} (55%) create mode 100644 src/Microsoft.AspNet.SpaServices/Prerendering/Prerenderer.cs rename src/Microsoft.AspNet.SpaServices/{ => Routing}/SpaRouteConstraint.cs (96%) rename src/Microsoft.AspNet.SpaServices/{ => Routing}/SpaRouteExtensions.cs (100%) rename src/Microsoft.AspNet.SpaServices/{ => Webpack}/WebpackDevMiddleware.cs (98%) rename src/Microsoft.AspNet.SpaServices/{ => Webpack}/WebpackDevMiddlewareOptions.cs (77%) diff --git a/samples/react/MusicStore/ReactApp/boot-server.tsx b/samples/react/MusicStore/ReactApp/boot-server.tsx index a3d7c81..c0df3ab 100644 --- a/samples/react/MusicStore/ReactApp/boot-server.tsx +++ b/samples/react/MusicStore/ReactApp/boot-server.tsx @@ -3,34 +3,38 @@ import { Provider } from 'react-redux'; import { renderToString } from 'react-dom/server'; import { match, RouterContext } from 'react-router'; import createMemoryHistory from 'history/lib/createMemoryHistory'; -React; - import { routes } from './routes'; import configureStore from './configureStore'; -import { ApplicationState } from './store'; +React; -export default function (params: any, callback: (err: any, result: { html: string, state: any }) => void) { - const { location } = params; - match({ routes, location }, (error, redirectLocation, renderProps: any) => { - try { +export default function (params: any): Promise<{ html: string }> { + return new Promise<{ html: string, globals: { [key: string]: any } }>((resolve, reject) => { + // Match the incoming request against the list of client-side routes + match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { if (error) { throw error; } + // Build an instance of the application const history = createMemoryHistory(params.url); - const store = params.state as Redux.Store || configureStore(history); - let html = renderToString( + const store = configureStore(history); + const app = ( - + ); - - // Also serialise the Redux state so the client can pick up where the server left off - html += ``; - - callback(null, { html, state: store }); - } catch (error) { - callback(error, null); - } + + // Perform an initial render that will cause any async tasks (e.g., data access) to begin + renderToString(app); + + // Once the tasks are done, we can perform the final render + // We also send the redux store state, so the client can continue execution where the server left off + params.domainTasks.then(() => { + resolve({ + html: renderToString(app), + globals: { initialReduxState: store.getState() } + }); + }, reject); // Also propagate any errors back into the host application + }); }); } diff --git a/samples/react/MusicStore/ReactApp/boot.tsx b/samples/react/MusicStore/ReactApp/boot.tsx index 6a8b6fc..ad8f367 100644 --- a/samples/react/MusicStore/ReactApp/boot.tsx +++ b/samples/react/MusicStore/ReactApp/boot.tsx @@ -8,8 +8,10 @@ import './styles/styles.css'; import 'bootstrap/dist/css/bootstrap.css'; import configureStore from './configureStore'; import { routes } from './routes'; +import { ApplicationState } from './store'; -const store = configureStore(browserHistory); +const initialState = (window as any).initialReduxState as ApplicationState; +const store = configureStore(browserHistory, initialState); ReactDOM.render( diff --git a/samples/react/MusicStore/ReactApp/fx/render-server.js b/samples/react/MusicStore/ReactApp/fx/render-server.js deleted file mode 100644 index 5ea8c35..0000000 --- a/samples/react/MusicStore/ReactApp/fx/render-server.js +++ /dev/null @@ -1,54 +0,0 @@ -require('./require-ts-babel')(); // Enable loading TS/TSX/JSX/ES2015 modules -var url = require('url'); -var domainTask = require('domain-task'); -var baseUrl = require('domain-task/fetch').baseUrl; - -function render(bootModulePath, absoluteRequestUrl, requestPathAndQuery, callback) { - var bootFunc = require(bootModulePath); - if (typeof bootFunc !== 'function') { - bootFunc = bootFunc.default; - } - if (typeof bootFunc !== 'function') { - throw new Error('The module at ' + bootModulePath + ' must export a default function, otherwise we don\'t know how to invoke it.') - } - - var params = { - location: url.parse(requestPathAndQuery), - url: requestPathAndQuery, - state: undefined - }; - - // Open a new domain that can track all the async tasks commenced during first render - domainTask.run(function() { - baseUrl(absoluteRequestUrl); - - // Since route matching is asynchronous, add the rendering itself to the list of tasks we're awaiting - domainTask.addTask(new Promise(function (resolve, reject) { - // Now actually perform the first render that will match a route and commence associated tasks - bootFunc(params, function(error, result) { - if (error) { - reject(error); - } else { - // The initial 'loading' state HTML is irrelevant - we only want to capture the state - // so we can use it to perform a real render once all data is loaded - params.state = result.state; - resolve(); - } - }); - })); - }, function(error) { - // By now, all the data should be loaded, so we can render for real based on the state now - // TODO: Add an optimisation where, if domain-tasks had no outstanding tasks at the end of - // the previous render, we don't re-render (we can use the previous html and state). - if (error) { console.error(error); throw error; } - bootFunc(params, callback); - }); -} - -render('../boot-server.tsx', 'http://localhost:5000', '/', (err, html) => { - if (err) { - throw err; - } - - console.log(html); -}); diff --git a/samples/react/MusicStore/ReactApp/fx/require-ts-babel.js b/samples/react/MusicStore/ReactApp/fx/require-ts-babel.js deleted file mode 100644 index 8656138..0000000 --- a/samples/react/MusicStore/ReactApp/fx/require-ts-babel.js +++ /dev/null @@ -1,44 +0,0 @@ -var fs = require('fs'); -var ts = require('ntypescript'); -var babelCore = require('babel-core'); -var resolveBabelRc = require('babel-loader/lib/resolve-rc'); // If this ever breaks, we can easily scan up the directory hierarchy ourselves -var origJsLoader = require.extensions['.js']; - -function resolveBabelOptions(relativeToFilename) { - var babelRcText = resolveBabelRc(relativeToFilename); - return babelRcText ? JSON.parse(babelRcText) : {}; -} - -function loadViaTypeScript(module, filename) { - // First perform a minimal transpilation from TS code to ES2015. This is very fast (doesn't involve type checking) - // and is unlikely to need any special compiler options - var src = fs.readFileSync(filename, 'utf8'); - var compilerOptions = { jsx: ts.JsxEmit.Preserve, module: ts.ModuleKind.ES2015, target: ts.ScriptTarget.ES6 }; - var es6Code = ts.transpile(src, compilerOptions, 'test.tsx', /* diagnostics */ []); - - // Second, process the ES2015 via Babel. We have to do this (instead of going directly from TS to ES5) because - // TypeScript's ES5 output isn't exactly compatible with Node-style CommonJS modules. The main issue is with - // resolving default exports - https://github.com/Microsoft/TypeScript/issues/2719 - var es5Code = babelCore.transform(es6Code, resolveBabelOptions(filename)).code; - return module._compile(es5Code, filename); -} - -function loadViaBabel(module, filename) { - // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. - // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict - // mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode). - var useBabel = filename.indexOf('node_modules') < 0; - if (useBabel) { - var transformedFile = babelCore.transformFileSync(filename, resolveBabelOptions(filename)); - return module._compile(transformedFile.code, filename); - } else { - return origJsLoader.apply(this, arguments); - } -} - -module.exports = function register() { - require.extensions['.js'] = loadViaBabel; - require.extensions['.jsx'] = loadViaBabel; - require.extensions['.ts'] = loadViaTypeScript; - require.extensions['.tsx'] = loadViaTypeScript; -} diff --git a/samples/react/MusicStore/Startup.cs b/samples/react/MusicStore/Startup.cs index c36fb56..3e601f5 100755 --- a/samples/react/MusicStore/Startup.cs +++ b/samples/react/MusicStore/Startup.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Identity.EntityFramework; -using Microsoft.AspNet.SpaServices; +using Microsoft.AspNet.SpaServices.Webpack; using Microsoft.Data.Entity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/samples/react/MusicStore/Views/Home/Index.cshtml b/samples/react/MusicStore/Views/Home/Index.cshtml index db4dc4f..543e8ba 100755 --- a/samples/react/MusicStore/Views/Home/Index.cshtml +++ b/samples/react/MusicStore/Views/Home/Index.cshtml @@ -2,7 +2,7 @@ ViewData["Title"] = "Home Page"; } -
Loading...
+
@section scripts { diff --git a/samples/react/MusicStore/Views/_ViewImports.cshtml b/samples/react/MusicStore/Views/_ViewImports.cshtml index a07c195..89b9e8f 100755 --- a/samples/react/MusicStore/Views/_ViewImports.cshtml +++ b/samples/react/MusicStore/Views/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using MusicStore @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" +@addTagHelper "*, Microsoft.AspNet.SpaServices" diff --git a/src/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index da81202..d833644 100644 --- a/src/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -4,7 +4,7 @@ var http = require('http'); var path = require('path'); var requestedPortOrZero = parseInt(process.argv[2]) || 0; // 0 means 'let the OS decide' -autoQuitOnFileChange(process.cwd(), ['.js', '.json', '.html']); +autoQuitOnFileChange(process.cwd(), ['.js', '.jsx', '.ts', '.tsx', '.json', '.html']); var server = http.createServer(function(req, res) { readRequestBodyAsJson(req, function(bodyJson) { diff --git a/src/Microsoft.AspNet.ReactServices/ReactRenderer.cs b/src/Microsoft.AspNet.ReactServices/ReactRenderer.cs deleted file mode 100644 index e97f39e..0000000 --- a/src/Microsoft.AspNet.ReactServices/ReactRenderer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNet.NodeServices; - -namespace Microsoft.AspNet.ReactServices -{ - public static class ReactRenderer - { - private static StringAsTempFile nodeScript; - - static ReactRenderer() { - // Consider populating this lazily - var script = EmbeddedResourceReader.Read(typeof (ReactRenderer), "/Content/Node/react-rendering.js"); - nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit - } - - public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestUrl) { - return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { - moduleName = componentModuleName, - exportName = componentExportName, - requestUrl = requestUrl - }); - } - } -} diff --git a/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js b/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js new file mode 100644 index 0000000..23644a0 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js @@ -0,0 +1,135 @@ +// ----------------------------------------------------------------------------------------- +// Prepare the Node environment to support loading .jsx/.ts/.tsx files without needing precompilation, +// since that's such a common scenario. In the future, this might become a config option. +// This is bundled in with the actual prerendering logic below just to simplify the initialization +// logic (we can't have cross-file imports, because these files don't exist on disk until the +// StringAsTempFile utility puts them there temporarily). + +// TODO: Consider some general method for checking if you have all the necessary NPM modules installed, +// and if not, giving an error that tells you what command to execute to install the missing ones. +var fs = require('fs'); +var ts = require('ntypescript'); +var babelCore = require('babel-core'); +var resolveBabelRc = require('babel-loader/lib/resolve-rc'); // If this ever breaks, we can easily scan up the directory hierarchy ourselves +var origJsLoader = require.extensions['.js']; + +function resolveBabelOptions(relativeToFilename) { + var babelRcText = resolveBabelRc(relativeToFilename); + return babelRcText ? JSON.parse(babelRcText) : {}; +} + +function loadViaTypeScript(module, filename) { + // First perform a minimal transpilation from TS code to ES2015. This is very fast (doesn't involve type checking) + // and is unlikely to need any special compiler options + var src = fs.readFileSync(filename, 'utf8'); + var compilerOptions = { jsx: ts.JsxEmit.Preserve, module: ts.ModuleKind.ES2015, target: ts.ScriptTarget.ES6 }; + var es6Code = ts.transpile(src, compilerOptions, 'test.tsx', /* diagnostics */ []); + + // Second, process the ES2015 via Babel. We have to do this (instead of going directly from TS to ES5) because + // TypeScript's ES5 output isn't exactly compatible with Node-style CommonJS modules. The main issue is with + // resolving default exports - https://github.com/Microsoft/TypeScript/issues/2719 + var es5Code = babelCore.transform(es6Code, resolveBabelOptions(filename)).code; + return module._compile(es5Code, filename); +} + +function loadViaBabel(module, filename) { + // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. + // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict + // mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode). + var useBabel = filename.indexOf('node_modules') < 0; + if (useBabel) { + var transformedFile = babelCore.transformFileSync(filename, resolveBabelOptions(filename)); + return module._compile(transformedFile.code, filename); + } else { + return origJsLoader.apply(this, arguments); + } +} + +function register() { + require.extensions['.js'] = loadViaBabel; + require.extensions['.jsx'] = loadViaBabel; + require.extensions['.ts'] = loadViaTypeScript; + require.extensions['.tsx'] = loadViaTypeScript; +}; + +register(); + +// ----------------------------------------------------------------------------------------- +// Rendering + +var url = require('url'); +var path = require('path'); +var domain = require('domain'); +var domainTask = require('domain-task'); +var baseUrl = require('domain-task/fetch').baseUrl; + +function findBootFunc(bootModulePath, bootModuleExport) { + var resolvedPath = path.resolve(process.cwd(), bootModulePath); + var bootFunc = require(resolvedPath); + if (bootModuleExport) { + bootFunc = bootFunc[bootModuleExport]; + } else if (typeof bootFunc !== 'function') { + bootFunc = bootFunc.default; // TypeScript sometimes uses this name for default exports + } + if (typeof bootFunc !== 'function') { + if (bootModuleExport) { + throw new Error('The module at ' + bootModulePath + ' has no function export named ' + bootModuleExport + '.'); + } else { + throw new Error('The module at ' + bootModulePath + ' does not export a default function, and you have not specified which export to invoke.'); + } + } + + return bootFunc; +} + +function renderToString(callback, bootModulePath, bootModuleExport, absoluteRequestUrl, requestPathAndQuery) { + var bootFunc = findBootFunc(bootModulePath, bootModuleExport); + + // Prepare a promise that will represent the completion of all domain tasks in this execution context. + // The boot code will wait for this before performing its final render. + var domainTaskCompletionPromiseResolve; + var domainTaskCompletionPromise = new Promise(function (resolve, reject) { + domainTaskCompletionPromiseResolve = resolve; + }); + var params = { + location: url.parse(requestPathAndQuery), + url: requestPathAndQuery, + domainTasks: domainTaskCompletionPromise + }; + + // Open a new domain that can track all the async tasks involved in the app's execution + domainTask.run(function() { + // Workaround for Node bug where native Promise continuations lose their domain context + // (https://github.com/nodejs/node-v0.x-archive/issues/8648) + bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain.active); + + // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context + baseUrl(absoluteRequestUrl); + + // Actually perform the rendering + bootFunc(params).then(function(successResult) { + callback(null, { html: successResult.html, globals: successResult.globals }); + }, function(error) { + callback(error, null); + }); + }, function allDomainTasksCompleted(error) { + // There are no more ongoing domain tasks (typically data access operations), so we can resolve + // the domain tasks promise which notifies the boot code that it can do its final render. + if (error) { + callback(error, null); + } else { + domainTaskCompletionPromiseResolve(); + } + }); +} + +function bindPromiseContinuationsToDomain(promise, domainInstance) { + var originalThen = promise.then; + promise.then = function then(resolve, reject) { + if (typeof resolve === 'function') { resolve = domainInstance.bind(resolve); } + if (typeof reject === 'function') { reject = domainInstance.bind(reject); } + return originalThen.call(this, resolve, reject); + }; +} + +module.exports.renderToString = renderToString; diff --git a/src/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs b/src/Microsoft.AspNet.SpaServices/Prerendering/PrerenderTagHelper.cs similarity index 55% rename from src/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs rename to src/Microsoft.AspNet.SpaServices/Prerendering/PrerenderTagHelper.cs index 6d2b761..5f3cc8b 100644 --- a/src/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs +++ b/src/Microsoft.AspNet.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -1,19 +1,22 @@ using System; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Extensions; using Microsoft.AspNet.NodeServices; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.Extensions.PlatformAbstractions; +using Newtonsoft.Json; -namespace Microsoft.AspNet.ReactServices +namespace Microsoft.AspNet.SpaServices.Prerendering { [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] - public class ReactPrerenderTagHelper : TagHelper + public class PrerenderTagHelper : TagHelper { static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI - const string PrerenderModuleAttributeName = "asp-react-prerender-module"; - const string PrerenderExportAttributeName = "asp-react-prerender-export"; + const string PrerenderModuleAttributeName = "asp-prerender-module"; + const string PrerenderExportAttributeName = "asp-prerender-export"; [HtmlAttributeName(PrerenderModuleAttributeName)] public string ModuleName { get; set; } @@ -24,7 +27,7 @@ namespace Microsoft.AspNet.ReactServices private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public ReactPrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor) + public PrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices; @@ -40,12 +43,27 @@ namespace Microsoft.AspNet.ReactServices public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var request = this.contextAccessor.HttpContext.Request; - var result = await ReactRenderer.RenderToString( + var result = await Prerenderer.RenderToString( nodeServices: this.nodeServices, componentModuleName: this.ModuleName, componentExportName: this.ExportName, - requestUrl: request.Path + request.QueryString.Value); - output.Content.SetHtmlContent(result); + requestAbsoluteUrl: UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request), + requestPathAndQuery: request.Path + request.QueryString.Value); + output.Content.SetHtmlContent(result.Html); + + // Also attach any specific globals to the 'window' object. This is useful for transferring + // general state between server and client. + if (result.Globals != null) { + var stringBuilder = new StringBuilder(); + foreach (var property in result.Globals.Properties()) { + stringBuilder.AppendFormat("window.{0} = {1};", + property.Name, + property.Value.ToString(Formatting.None)); + } + if (stringBuilder.Length > 0) { + output.PostElement.SetHtmlContent($""); + } + } } } } diff --git a/src/Microsoft.AspNet.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNet.SpaServices/Prerendering/Prerenderer.cs new file mode 100644 index 0000000..b7558d4 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/Prerendering/Prerenderer.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.NodeServices; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SpaServices.Prerendering +{ + public static class Prerenderer + { + private static Lazy nodeScript; + + static Prerenderer() { + nodeScript = new Lazy(() => { + var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); + return new StringAsTempFile(script); // Will be cleaned up on process exit + }); + } + + public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestAbsoluteUrl, string requestPathAndQuery) { + return await nodeServices.InvokeExport(nodeScript.Value.FileName, "renderToString", + /* bootModulePath */ componentModuleName, + /* bootModuleExport */ componentExportName, + /* absoluteRequestUrl */ requestAbsoluteUrl, + /* requestPathAndQuery */ requestPathAndQuery); + } + } + + public class RenderToStringResult { + public string Html; + public JObject Globals; + } +} diff --git a/src/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs b/src/Microsoft.AspNet.SpaServices/Routing/SpaRouteConstraint.cs similarity index 96% rename from src/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs rename to src/Microsoft.AspNet.SpaServices/Routing/SpaRouteConstraint.cs index d4df4c7..dcbd6b3 100644 --- a/src/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs +++ b/src/Microsoft.AspNet.SpaServices/Routing/SpaRouteConstraint.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; -namespace Microsoft.AspNet.SpaServices +namespace Microsoft.AspNet.SpaServices { internal class SpaRouteConstraint : IRouteConstraint { diff --git a/src/Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs b/src/Microsoft.AspNet.SpaServices/Routing/SpaRouteExtensions.cs similarity index 100% rename from src/Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs rename to src/Microsoft.AspNet.SpaServices/Routing/SpaRouteExtensions.cs diff --git a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs similarity index 98% rename from src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs rename to src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs index 864df52..6c71800 100644 --- a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNet.NodeServices; using Microsoft.AspNet.Proxy; -using Microsoft.AspNet.SpaServices; +using Microsoft.AspNet.SpaServices.Webpack; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; diff --git a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs similarity index 77% rename from src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs rename to src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs index 1fa5c56..a9510fc 100644 --- a/src/Microsoft.AspNet.SpaServices/WebpackDevMiddlewareOptions.cs +++ b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNet.SpaServices { +namespace Microsoft.AspNet.SpaServices.Webpack { public class WebpackDevMiddlewareOptions { public bool HotModuleReplacement { get; set; } public bool ReactHotModuleReplacement { get; set; }