├── TestApp ├── Views │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ └── Home │ │ └── Index.cshtml ├── wwwroot │ ├── favicon.ico │ └── dist │ │ ├── app.js.LICENSE.txt │ │ ├── vendor-manifest.json │ │ ├── vendor.js.LICENSE.txt │ │ └── app.js ├── appsettings.Development.json ├── appsettings.json ├── ClientApp │ ├── components │ │ ├── app │ │ │ └── app.tsx │ │ ├── counter │ │ │ └── index.tsx │ │ ├── navmenu │ │ │ ├── navmenu.css │ │ │ └── navmenu.tsx │ │ ├── home │ │ │ └── index.tsx │ │ └── fetchdata │ │ │ └── index.tsx │ ├── boot.js │ ├── boot.js.map │ └── boot.ts ├── webpack.config.test.js ├── TestApp.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── tsconfig.json ├── Controllers │ ├── SpaApp │ │ └── HomeController.cs │ └── SampleDataController.cs ├── Startup.cs ├── webpack.config.vendor.js ├── package.json └── webpack.config.js ├── .gitignore ├── BrunoLau.SpaServices ├── Webpack │ ├── ConditionalProxyMiddlewareOptions.cs │ ├── WebpackDevMiddlewareOptions.cs │ ├── ConditionalProxyMiddleware.cs │ └── WebpackDevMiddleware.cs ├── Common │ ├── EmbeddedResourceReader.cs │ └── NodeInteropFactory.cs ├── Prerendering │ ├── JavaScriptModuleExport.cs │ ├── ISpaPrerenderer.cs │ ├── DefaultSpaPrerenderer.cs │ ├── RenderToStringResult.cs │ └── Prerenderer.cs ├── BrunoLau.SpaServices.csproj └── Content │ └── Node │ ├── prerenderer.js │ └── webpack-dev-middleware.js ├── BrunoLau.SpaServices.Razor ├── PrerenderingServiceCollectionExtensions.cs ├── BrunoLau.SpaServices.RazorSpaUtils.csproj └── PrerenderTagHelper.cs ├── BrunoLau.SpaServices.sln └── README.md /TestApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /TestApp/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunolau/BrunoLau.SpaServices/HEAD/TestApp/wwwroot/favicon.ico -------------------------------------------------------------------------------- /TestApp/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using vuespa 2 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" 3 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices" 4 | -------------------------------------------------------------------------------- /TestApp/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | -------------------------------------------------------------------------------- /TestApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 |
Loading...
5 | @section scripts { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /TestApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TestApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | TestApp/bin/ 3 | TestApp/obj/ 4 | TestApp/node_modules/ 5 | TestApp/TestApp.csproj.user 6 | BrunoLau.SpaServices.Razor/bin/ 7 | BrunoLau.SpaServices.Razor/obj/ 8 | BrunoLau.SpaServices/bin/ 9 | BrunoLau.SpaServices/obj/ 10 | TestApp/package-lock.json 11 | TestApp/wwwroot/dist-dev/ 12 | -------------------------------------------------------------------------------- /TestApp/ClientApp/components/app/app.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Component } from 'vue-property-decorator' 3 | import NavMenu from '../navmenu/navmenu' 4 | 5 | @Component 6 | export default class AppEntryComponent extends Vue { 7 | render(h) { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /TestApp/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | var webpackConf = require('./webpack.config.js') 2 | delete webpackConf.entry 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | browsers: ['PhantomJS'], 7 | frameworks: ['jasmine'], 8 | reporters: ['spec'], 9 | files: ['./ClientTests/unit/index.js'], 10 | preprocessors: { 11 | './ClientTests/unit/index.js': ['webpack'] 12 | }, 13 | webpack: webpackConf, 14 | webpackMiddleware: { 15 | noInfo: true 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /TestApp/wwwroot/dist/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * @overview es6-promise - a tiny implementation of Promises/A+. 3 | * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) 4 | * @license Licensed under MIT license 5 | * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE 6 | * @version 3.3.1 7 | */ 8 | 9 | /** 10 | * vue-class-component v7.2.6 11 | * (c) 2015-present Evan You 12 | * @license MIT 13 | */ 14 | -------------------------------------------------------------------------------- /TestApp/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - vuespa 7 | 8 | 9 | 10 | 11 | @RenderBody() 12 | 13 | 14 | @RenderSection("scripts", required: false) 15 | 16 | 17 | -------------------------------------------------------------------------------- /TestApp/wwwroot/dist/vendor-manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"vendor_f22b82494aa4bc4a64cf","content":{"./node_modules/bootstrap/dist/css/bootstrap.min.css":{"id":79,"buildMeta":{"exportsType":"namespace","sideEffectFree":true},"exports":[]},"./node_modules/vue/dist/vue.runtime.esm.js":{"id":144,"buildMeta":{"exportsType":"namespace"},"exports":["default"]},"./node_modules/vue-router/dist/vue-router.esm.js":{"id":345,"buildMeta":{"exportsType":"namespace"},"exports":["default"]},"./node_modules/event-source-polyfill/src/eventsource.js":{"id":541,"buildMeta":{}},"./node_modules/bootstrap/dist/js/bootstrap.js":{"id":734,"buildMeta":{}},"./node_modules/jquery/dist/jquery.js":{"id":755,"buildMeta":{}}}} -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BrunoLau.SpaServices.Webpack 4 | { 5 | internal class ConditionalProxyMiddlewareOptions 6 | { 7 | public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout) 8 | { 9 | Scheme = scheme; 10 | Host = host; 11 | Port = port; 12 | RequestTimeout = requestTimeout; 13 | } 14 | 15 | public string Scheme { get; } 16 | public string Host { get; } 17 | public string Port { get; } 18 | public TimeSpan RequestTimeout { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /TestApp/TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | 3.9 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 26c767aa-a91f-4f51-bed8-6a4c4ee9ba9c 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /TestApp/ClientApp/components/counter/index.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Component } from 'vue-property-decorator' 3 | 4 | @Component 5 | export default class CounterComponent extends Vue { 6 | currentcount: number = 5; 7 | 8 | incrementCounter() { 9 | this.currentcount++; 10 | } 11 | 12 | render(h) { 13 | return ( 14 |
15 |

Counter TSX

16 |

Counter done the TSX + Vue.js 2 way

17 |

Current count: {this.currentcount}

18 | 19 |
20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /TestApp/ClientApp/boot.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap'; 2 | import Vue from 'vue'; 3 | import VueRouter from 'vue-router'; 4 | window['Promise'] = require('es6-promise').Promise; 5 | Vue.use(VueRouter); 6 | var routes = [ 7 | { path: '/', component: (function () { return import('./components/home'); }) }, 8 | { path: '/counter', component: (function () { return import('./components/counter'); }) }, 9 | { path: '/fetchdata', component: (function () { return import('./components/fetchdata'); }) }, 10 | ]; 11 | new Vue({ 12 | el: '#app-root', 13 | router: new VueRouter({ mode: 'history', routes: routes }), 14 | render: function (h) { return h(require('./components/app/app.vue')); } 15 | }); 16 | //# sourceMappingURL=boot.js.map -------------------------------------------------------------------------------- /TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace TestApp 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TestApp/ClientApp/boot.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"boot.js","sourceRoot":"","sources":["boot.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,CAAC;AACnB,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,MAAM,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC;AACnD,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAGnB,IAAM,MAAM,GAAG;IACX,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,cAAM,OAAA,MAAM,CAAC,mBAAmB,CAAC,EAA3B,CAA2B,CAAC,EAAE;IAC7D,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,cAAM,OAAA,MAAM,CAAC,sBAAsB,CAAC,EAA9B,CAA8B,CAAC,EAAE;IACvE,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,cAAM,OAAA,MAAM,CAAC,wBAAwB,CAAC,EAAhC,CAAgC,CAAC,EAAE;CAE9E,CAAC;AAEF,IAAI,GAAG,CAAC;IACJ,EAAE,EAAE,WAAW;IACf,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1D,MAAM,EAAE,UAAA,CAAC,IAAI,OAAA,CAAC,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC,EAAtC,CAAsC;CACtD,CAAC,CAAC"} -------------------------------------------------------------------------------- /TestApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54798", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "TestApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "hotReloadEnabled": false, 25 | "applicationUrl": "http://localhost:5000", 26 | "dotnetRunMessages": "true" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /TestApp/ClientApp/boot.ts: -------------------------------------------------------------------------------- 1 | import 'bootstrap'; 2 | import Vue from 'vue'; 3 | import VueRouter from 'vue-router'; 4 | import AppEntryComponent from './components/app/app'; 5 | import CounterComponent from './components/counter'; 6 | import HomeComponent from './components/home'; 7 | import FetchDataComponent from './components/fetchdata'; 8 | 9 | if (!window['Promise']) { 10 | window['Promise'] = require('es6-promise').Promise; 11 | } 12 | 13 | Vue.use(VueRouter); 14 | 15 | 16 | const routes = [ 17 | { path: '/', component: HomeComponent }, 18 | { path: '/counter', component: CounterComponent }, 19 | { path: '/fetchdata', component: FetchDataComponent }, 20 | 21 | ]; 22 | 23 | new Vue({ 24 | el: '#app-root', 25 | router: new VueRouter({ mode: 'history', routes: routes }), 26 | render: h => h(AppEntryComponent) 27 | }); 28 | -------------------------------------------------------------------------------- /TestApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "strictNullChecks": false, 5 | "allowSyntheticDefaultImports": true, 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "target": "es5", 12 | "sourceMap": true, 13 | "skipDefaultLibCheck": true, 14 | "jsx": "preserve", 15 | //"jsx": "react", 16 | //"jsxFactory": "h", 17 | "strict": true, 18 | "types": [ "webpack-env", "node" ], 19 | "lib": [ "es2015.promise", "ES6", "es5", "dom" ] 20 | }, 21 | "exclude": [ 22 | "bin", 23 | "node_modules", 24 | "./node_modules", 25 | "wwwroot", 26 | "./wwwroot" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices.Razor/PrerenderingServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | 2 | using BrunoLau.SpaServices.Prerendering; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection 5 | { 6 | /// 7 | /// Extension methods for setting up prerendering features in an . 8 | /// 9 | public static class PrerenderingServiceCollectionExtensions 10 | { 11 | /// 12 | /// Configures the dependency injection system to supply an implementation 13 | /// of . 14 | /// 15 | /// The . 16 | public static void AddSpaPrerenderer(this IServiceCollection serviceCollection) 17 | { 18 | serviceCollection.AddHttpContextAccessor(); 19 | serviceCollection.AddSingleton(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TestApp/Controllers/SpaApp/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace TestApp.Controllers 9 | { 10 | public class HomeController : Controller 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public HomeController(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | 20 | public IActionResult Index() 21 | { 22 | return View(); 23 | } 24 | 25 | [Route("fetchdata")] 26 | public IActionResult FetchData() 27 | { 28 | return View("~/Views/Home/Index.cshtml"); 29 | } 30 | 31 | [Route("counter")] 32 | public IActionResult Counter() 33 | { 34 | return View("~/Views/Home/Index.cshtml"); 35 | } 36 | 37 | public IActionResult Error() 38 | { 39 | return View(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices.Razor/BrunoLau.SpaServices.RazorSpaUtils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 6.0.1 5 | Bruno Laurinec 6 | Bruno Laurinec 7 | Library contains the PrerenderTagHelper class. Port of Microsoft.AspNetCore.SpaServices into .NET 5 by using the Jering.Javascript.NodeJS interop library 8 | Initially written by Microsoft, ported by Bruno Laurinec 9 | nodeservices, webpack 10 | https://github.com/brunolau/BrunoLau.SpaServices 11 | https://github.com/brunolau/BrunoLau.SpaServices 12 | true 13 | Apache-2.0 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Common/EmbeddedResourceReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace BrunoLau.SpaServices.Common 8 | { 9 | internal static class EmbeddedResourceReader 10 | { 11 | /// 12 | /// Reads the specified embedded resource from a given assembly. 13 | /// 14 | /// Any in the assembly whose resource is to be read. 15 | /// The path of the resource to be read. 16 | /// The contents of the resource. 17 | public static string Read(Type assemblyContainingType, string path) 18 | { 19 | var asm = assemblyContainingType.GetTypeInfo().Assembly; 20 | var embeddedResourceName = asm.GetName().Name + path.Replace("/", "."); 21 | 22 | using (var stream = asm.GetManifestResourceStream(embeddedResourceName)) 23 | using (var sr = new StreamReader(stream)) 24 | { 25 | return sr.ReadToEnd(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Prerendering/JavaScriptModuleExport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BrunoLau.SpaServices.Prerendering 4 | { 5 | /// 6 | /// Describes how to find the JavaScript code that performs prerendering. 7 | /// 8 | public class JavaScriptModuleExport 9 | { 10 | /// 11 | /// Creates a new instance of . 12 | /// 13 | /// The path to the JavaScript module containing prerendering code. 14 | public JavaScriptModuleExport(string moduleName) 15 | { 16 | ModuleName = moduleName; 17 | } 18 | 19 | /// 20 | /// Specifies the path to the JavaScript module containing prerendering code. 21 | /// 22 | public string ModuleName { get; private set; } 23 | 24 | /// 25 | /// If set, specifies the name of the CommonJS export that is the prerendering function to execute. 26 | /// If not set, the JavaScript module's default CommonJS export must itself be the prerendering function. 27 | /// 28 | public string ExportName { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /TestApp/Controllers/SampleDataController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace vuespa.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | public class SampleDataController : Controller 11 | { 12 | private static string[] Summaries = new[] 13 | { 14 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 15 | }; 16 | 17 | [HttpGet("[action]")] 18 | public IEnumerable WeatherForecasts() 19 | { 20 | var rng = new Random(); 21 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 22 | { 23 | DateFormatted = DateTime.Now.AddDays(index).ToString("d"), 24 | TemperatureC = rng.Next(-20, 55), 25 | Summary = Summaries[rng.Next(Summaries.Length)] 26 | }); 27 | } 28 | 29 | public class WeatherForecast 30 | { 31 | public string DateFormatted { get; set; } 32 | public int TemperatureC { get; set; } 33 | public string Summary { get; set; } 34 | 35 | public int TemperatureF 36 | { 37 | get 38 | { 39 | return 32 + (int)(TemperatureC / 0.5556); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/BrunoLau.SpaServices.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Port of Microsoft.AspNetCore.SpaServices into .NET 8 by using the Jering.Javascript.NodeJS interop library 4 | net8.0 5 | 6 | true 7 | 8.0.0 8 | Bruno Laurinec 9 | 10 | Initially written by Microsoft, ported by Bruno Laurinec 11 | nodeservices, webpack 12 | https://github.com/brunolau/BrunoLau.SpaServices 13 | https://github.com/brunolau/BrunoLau.SpaServices 14 | Apache-2.0 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Prerendering/ISpaPrerenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BrunoLau.SpaServices.Prerendering 4 | { 5 | /// 6 | /// Represents a service that can perform server-side prerendering for 7 | /// JavaScript-based Single Page Applications. This is an alternative 8 | /// to using the 'asp-prerender-module' tag helper. 9 | /// 10 | public interface ISpaPrerenderer 11 | { 12 | /// 13 | /// Invokes JavaScript code to perform server-side prerendering for a 14 | /// Single-Page Application. This is an alternative to using the 15 | /// 'asp-prerender-module' tag helper. 16 | /// 17 | /// The JavaScript module that exports a prerendering function. 18 | /// The name of the export from the JavaScript module, if it is not the default export. 19 | /// An optional JSON-serializable object to pass to the JavaScript prerendering function. 20 | /// If specified, the prerendering task will time out after this duration if not already completed. 21 | /// 22 | Task RenderToString( 23 | string moduleName, 24 | string exportName = null, 25 | object customDataParameter = null, 26 | int timeoutMilliseconds = default(int)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TestApp/ClientApp/components/navmenu/navmenu.css: -------------------------------------------------------------------------------- 1 | .main-nav li .glyphicon { 2 | margin-right: 10px; 3 | } 4 | 5 | /* Highlighting rules for nav menu items */ 6 | .main-nav li a.router-link-active, 7 | .main-nav li a.router-link-active:hover, 8 | .main-nav li a.router-link-active:focus { 9 | background-color: #4189C7; 10 | color: white; 11 | } 12 | 13 | /* Keep the nav menu independent of scrolling and on top of other items */ 14 | .main-nav { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | z-index: 1; 20 | } 21 | 22 | @media (max-width: 767px) { 23 | /* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */ 24 | body { 25 | padding-top: 50px; 26 | } 27 | } 28 | 29 | @media (min-width: 768px) { 30 | /* On small screens, convert the nav menu to a vertical sidebar */ 31 | .main-nav { 32 | height: 100%; 33 | width: calc(25% - 20px); 34 | } 35 | .main-nav .navbar { 36 | border-radius: 0px; 37 | border-width: 0px; 38 | height: 100%; 39 | } 40 | .main-nav .navbar-header { 41 | float: none; 42 | } 43 | .main-nav .navbar-collapse { 44 | border-top: 1px solid #444; 45 | padding: 0px; 46 | } 47 | .main-nav .navbar ul { 48 | float: none; 49 | } 50 | .main-nav .navbar li { 51 | float: none; 52 | font-size: 15px; 53 | margin: 6px; 54 | } 55 | .main-nav .navbar li a { 56 | padding: 10px 16px; 57 | border-radius: 4px; 58 | } 59 | .main-nav .navbar a { 60 | /* If a menu item's text is too long, truncate it */ 61 | width: 100%; 62 | white-space: nowrap; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | } 66 | } -------------------------------------------------------------------------------- /TestApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using BrunoLau.SpaServices.Webpack; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace TestApp 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | services.AddControllersWithViews(); 27 | } 28 | 29 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 30 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 31 | { 32 | if (env.IsDevelopment()) 33 | { 34 | app.UseDeveloperExceptionPage(); 35 | app.UseWebpackDevMiddlewareEx(new WebpackDevMiddlewareOptions 36 | { 37 | TryPatchHotModulePackage = true, 38 | HotModuleReplacement = true 39 | }); 40 | } 41 | else 42 | { 43 | app.UseExceptionHandler("/Home/Error"); 44 | } 45 | 46 | app.UseStaticFiles(); 47 | app.UseRouting(); 48 | app.UseEndpoints(endpoints => 49 | { 50 | endpoints.MapControllerRoute( 51 | name: "default", 52 | pattern: "{controller=Home}/{action=Index}/{id?}"); 53 | }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TestApp/ClientApp/components/home/index.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Component } from 'vue-property-decorator' 3 | 4 | @Component 5 | export default class HomeComponent extends Vue { 6 | render(h) { 7 | return ( 8 |
9 |

Hello, world!

10 |

Welcome to your new single-page application, built with:

11 | 17 |

To help you get started, we've also set up:

18 |
    19 |
  • Client-side navigation. For example, click Counter then Back to return here.
  • 20 |
  • Webpack dev middleware. In development mode, there's no need to run the webpack build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.
  • 21 |
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Vue app will be rebuilt and a new instance injected is into the page.
  • 22 |
  • Efficient production builds. In production mode, development-time features are disabled, and the webpack build tool produces minified static CSS and JavaScript files.
  • 23 |
24 |
25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Prerendering/DefaultSpaPrerenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Jering.Javascript.NodeJS; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace BrunoLau.SpaServices.Prerendering 8 | { 9 | /// 10 | /// Default implementation of a DI service that provides convenient access to 11 | /// server-side prerendering APIs. This is an alternative to prerendering via 12 | /// the asp-prerender-module tag helper. 13 | /// 14 | public class DefaultSpaPrerenderer : ISpaPrerenderer 15 | { 16 | private readonly string _applicationBasePath; 17 | private readonly CancellationToken _applicationStoppingToken; 18 | private readonly IHttpContextAccessor _httpContextAccessor; 19 | private readonly INodeJSService _nodeServices; 20 | 21 | public DefaultSpaPrerenderer( 22 | INodeJSService nodeServices, 23 | IHostApplicationLifetime applicationLifetime, 24 | IHostEnvironment hostingEnvironment, 25 | IHttpContextAccessor httpContextAccessor) 26 | { 27 | _applicationBasePath = hostingEnvironment.ContentRootPath; 28 | _applicationStoppingToken = applicationLifetime.ApplicationStopping; 29 | _httpContextAccessor = httpContextAccessor; 30 | _nodeServices = nodeServices; 31 | } 32 | 33 | public Task RenderToString( 34 | string moduleName, 35 | string exportName = null, 36 | object customDataParameter = null, 37 | int timeoutMilliseconds = default(int)) 38 | { 39 | return Prerenderer.RenderToString( 40 | _applicationBasePath, 41 | _nodeServices, 42 | _applicationStoppingToken, 43 | new JavaScriptModuleExport(moduleName) { ExportName = exportName }, 44 | _httpContextAccessor.HttpContext, 45 | customDataParameter, 46 | timeoutMilliseconds); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TestApp/webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | module.exports = (env) => { 6 | const isDevBuild = (env == null || env.production != true); 7 | 8 | return [{ 9 | stats: { modules: false }, 10 | resolve: { extensions: ['.js'] }, 11 | entry: { 12 | vendor: [ 13 | 'bootstrap', 14 | 'bootstrap/dist/css/bootstrap.min.css', 15 | 'event-source-polyfill', 16 | 'jquery', 17 | 'vue', 18 | 'vue-router' 19 | ], 20 | }, 21 | module: { 22 | rules: [ 23 | { test: /\.css(\?|$)/, use: [isDevBuild ? "style-loader" : MiniCssExtractPlugin.loader, "css-loader"] }, 24 | { test: /\.(png)(\?|$)/, use: 'url-loader?limit=100000' }, 25 | { 26 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 27 | use: [{ 28 | loader: 'file-loader', 29 | options: { 30 | name: '[name].[ext]', 31 | outputPath: 'fonts/' 32 | } 33 | }] 34 | } 35 | 36 | ] 37 | }, 38 | output: { 39 | path: path.join(__dirname, 'wwwroot', 'dist'), 40 | publicPath: '/dist/', 41 | filename: '[name].js', 42 | library: '[name]_[hash]' 43 | }, 44 | optimization: { 45 | minimize: !isDevBuild 46 | }, 47 | plugins: [ 48 | new MiniCssExtractPlugin(), 49 | new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) 50 | new webpack.DllPlugin({ 51 | path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), 52 | name: '[name]_[hash]' 53 | }) 54 | ] 55 | }]; 56 | }; 57 | -------------------------------------------------------------------------------- /TestApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuespa", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build-vendor": "node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env production --mode=production", 6 | "build-vendor-icons": "node node_modules/webpack/bin/webpack.js --config webpack.config.icons.js --env production --mode=production", 7 | "build-app-dev": "node node_modules/webpack/bin/webpack.js --env development --mode=development", 8 | "build-app-prod": "node node_modules/webpack/bin/webpack.js --env production --mode=production", 9 | "build-dev-vendor": "webpack --config webpack.config.vendor.js", 10 | "build-prod-vendor": "webpack. --config webpack.config.vendor.js --env production --mode=production", 11 | "build-dev": "webpack", 12 | "build-prod": "webpack --env.prod", 13 | "test": "karma start webpack.config.test.js --single-run" 14 | }, 15 | "dependencies": { 16 | "@types/node": "^12.20.4", 17 | "@types/webpack-env": "1.13.9", 18 | "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", 19 | "@vue/babel-preset-jsx": "^1.2.4", 20 | "acorn": "^8.0.5", 21 | "popper.js": "^1.16.1", 22 | "vue": "2.6.14", 23 | "vuex": "3.1.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "7.17.6", 27 | "@babel/core": "7.17.9", 28 | "@babel/preset-env": "7.16.11", 29 | "@types/node": "^12.20.4", 30 | "@types/webpack-env": "1.13.9", 31 | "babel-loader": "8.2.4", 32 | "bootstrap": "4.3.1", 33 | "connect": "3.7.0", 34 | "css-loader": "6.7.1", 35 | "event-source-polyfill": "1.0.7", 36 | "file-loader": "6.2.0", 37 | "jquery": "^3.6.0", 38 | "mini-css-extract-plugin": "1.6.2", 39 | "style-loader": "0.23.1", 40 | "ts-loader": "8.3.0", 41 | "typescript": "3.8.2", 42 | "url-loader": "4.1.1", 43 | "vue-class-component": "7.2.6", 44 | "vue-property-decorator": "9.1.2", 45 | "vue-router": "3.5.3", 46 | "webpack": "5.72.0", 47 | "webpack-cli": "4.9.2", 48 | "webpack-dev-middleware": "5.3.1", 49 | "webpack-hot-middleware": "2.25.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30717.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrunoLau.SpaServices", "BrunoLau.SpaServices\BrunoLau.SpaServices.csproj", "{9D9F3215-80CD-49D4-84D6-04649488903A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrunoLau.SpaServices.RazorSpaUtils", "BrunoLau.SpaServices.Razor\BrunoLau.SpaServices.RazorSpaUtils.csproj", "{C8C2887F-6864-4A2F-892A-34C442E85E90}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.csproj", "{8AFF9BDE-419E-4B97-967D-43DD4D4C1014}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {9D9F3215-80CD-49D4-84D6-04649488903A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {9D9F3215-80CD-49D4-84D6-04649488903A}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {9D9F3215-80CD-49D4-84D6-04649488903A}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {9D9F3215-80CD-49D4-84D6-04649488903A}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {C8C2887F-6864-4A2F-892A-34C442E85E90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {C8C2887F-6864-4A2F-892A-34C442E85E90}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {C8C2887F-6864-4A2F-892A-34C442E85E90}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {C8C2887F-6864-4A2F-892A-34C442E85E90}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {8AFF9BDE-419E-4B97-967D-43DD4D4C1014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {8AFF9BDE-419E-4B97-967D-43DD4D4C1014}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {8AFF9BDE-419E-4B97-967D-43DD4D4C1014}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8AFF9BDE-419E-4B97-967D-43DD4D4C1014}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {B3376030-E98F-4B8D-B8B3-5FAF15C60E44} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /TestApp/ClientApp/components/navmenu/navmenu.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Component } from 'vue-property-decorator' 3 | import './navmenu.css'; 4 | 5 | @Component 6 | export default class NavMenu extends Vue { 7 | render(h) { 8 | return ( 9 |
10 | 43 |
44 | 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /TestApp/ClientApp/components/fetchdata/index.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Component } from 'vue-property-decorator'; 3 | 4 | interface WeatherForecast { 5 | dateFormatted: string; 6 | temperatureC: number; 7 | temperatureF: number; 8 | summary: string; 9 | } 10 | 11 | @Component 12 | export default class FetchDataComponent extends Vue { 13 | forecasts: WeatherForecast[] = []; 14 | loading: boolean = true; 15 | 16 | async mounted() { 17 | await this.refreshData(); 18 | } 19 | 20 | async refreshData() { 21 | this.forecasts = await this.getDataAsync(); 22 | } 23 | 24 | async getDataAsync() { 25 | this.loading = true; 26 | let response = await fetch('/api/SampleData/WeatherForecasts'); 27 | let data = response.json(); 28 | this.loading = false; 29 | return data; 30 | } 31 | 32 | render(h) { 33 | return ( 34 |
35 |

Weather forecast

36 |

37 | This component demonstrates fetching data from the server. 38 | 39 |

40 | 41 |
42 | 43 | {this.renderForecastsTable(h)} 44 |
45 | ) 46 | } 47 | 48 | renderForecastsTable(h) { 49 | if (!this.loading) { 50 | return 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {this.forecasts.map(forecast => 61 | 62 | 63 | 64 | 65 | 66 | 67 | )} 68 | 69 |
DateTemp. (C)Temp. (F)Summary
{forecast.dateFormatted}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
; 70 | } else { 71 | return

Loading...

72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TestApp/wwwroot/dist/vendor.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Sizzle CSS Selector Engine v2.3.4 9 | * https://sizzlejs.com/ 10 | * 11 | * Copyright JS Foundation and other contributors 12 | * Released under the MIT license 13 | * https://js.foundation/ 14 | * 15 | * Date: 2019-04-08 16 | */ 17 | 18 | /*! 19 | * Vue.js v2.6.14 20 | * (c) 2014-2021 Evan You 21 | * Released under the MIT License. 22 | */ 23 | 24 | /*! 25 | * jQuery JavaScript Library v3.4.1 26 | * https://jquery.com/ 27 | * 28 | * Includes Sizzle.js 29 | * https://sizzlejs.com/ 30 | * 31 | * Copyright JS Foundation and other contributors 32 | * Released under the MIT license 33 | * https://jquery.org/license 34 | * 35 | * Date: 2019-05-01T21:04Z 36 | */ 37 | 38 | /** @license 39 | * eventsource.js 40 | * Available under MIT License (MIT) 41 | * https://github.com/Yaffle/EventSource/ 42 | */ 43 | 44 | /**! 45 | * @fileOverview Kickass library to create and place poppers near their reference elements. 46 | * @version 1.16.1 47 | * @license 48 | * Copyright (c) 2016 Federico Zivolo and contributors 49 | * 50 | * Permission is hereby granted, free of charge, to any person obtaining a copy 51 | * of this software and associated documentation files (the "Software"), to deal 52 | * in the Software without restriction, including without limitation the rights 53 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | * copies of the Software, and to permit persons to whom the Software is 55 | * furnished to do so, subject to the following conditions: 56 | * 57 | * The above copyright notice and this permission notice shall be included in all 58 | * copies or substantial portions of the Software. 59 | * 60 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | * SOFTWARE. 67 | */ 68 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Prerendering/RenderToStringResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace BrunoLau.SpaServices.Prerendering 5 | { 6 | /// 7 | /// Describes the prerendering result returned by JavaScript code. 8 | /// 9 | public class RenderToStringResult 10 | { 11 | /// 12 | /// If set, specifies JSON-serializable data that should be added as a set of global JavaScript variables in the document. 13 | /// This can be used to transfer arbitrary data from server-side prerendering code to client-side code (for example, to 14 | /// transfer the state of a Redux store). 15 | /// 16 | public JsonElement? Globals { get; set; } 17 | 18 | /// 19 | /// The HTML generated by the prerendering logic. 20 | /// 21 | public string Html { get; set; } 22 | 23 | /// 24 | /// If set, specifies that instead of rendering HTML, the response should be an HTTP redirection to this URL. 25 | /// This can be used if the prerendering code determines that the requested URL would lead to a redirection according 26 | /// to the SPA's routing configuration. 27 | /// 28 | public string RedirectUrl { get; set; } 29 | 30 | /// 31 | /// If set, specifies the HTTP status code that should be sent back with the server response. 32 | /// 33 | public int? StatusCode { get; set; } 34 | 35 | /// 36 | /// Constructs a block of JavaScript code that assigns data from the 37 | /// property to the global namespace. 38 | /// 39 | /// A block of JavaScript code. 40 | public string CreateGlobalsAssignmentScript() 41 | { 42 | if (Globals == null || Globals.Value.ValueKind == JsonValueKind.Null || Globals.Value.ValueKind == JsonValueKind.Undefined) 43 | { 44 | return string.Empty; 45 | } 46 | 47 | var stringBuilder = new StringBuilder(); 48 | 49 | foreach (var property in Globals.Value.EnumerateObject()) 50 | { 51 | stringBuilder.AppendFormat("window.{0} = {1};", 52 | property.Name, 53 | property.Value.ToString()); 54 | } 55 | 56 | return stringBuilder.ToString(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BrunoLau.SpaServices.Webpack 4 | { 5 | /// 6 | /// Options for configuring a Webpack dev middleware compiler. 7 | /// 8 | public class WebpackDevMiddlewareOptions 9 | { 10 | /// 11 | /// If true, hot module replacement (HMR) will be enabled. This automatically updates Webpack-built 12 | /// resources (such as JavaScript, CSS, or images) in your web browser whenever source files are changed. 13 | /// 14 | public bool HotModuleReplacement { get; set; } 15 | 16 | /// 17 | /// If set, overrides the URL that Webpack's client-side code will connect to when listening for updates. 18 | /// This must be a root-relative URL similar to "/__webpack_hmr" (which is the default endpoint). 19 | /// 20 | public string HotModuleReplacementEndpoint { get; set; } 21 | 22 | /// 23 | /// Overrides the internal port number that client-side HMR code will connect to. 24 | /// 25 | public int HotModuleReplacementServerPort { get; set; } 26 | 27 | /// 28 | /// If true, enables React-specific extensions to Webpack's hot module replacement (HMR) feature. 29 | /// This enables React components to be updated without losing their in-memory state. 30 | /// 31 | public bool ReactHotModuleReplacement { get; set; } 32 | 33 | /// 34 | /// If true, attempts to fix the webpack-hot-middleware package overlay problem 35 | /// 36 | public bool TryPatchHotModulePackage { get; set; } 37 | 38 | /// 39 | /// Specifies additional options to be passed to the Webpack Hot Middleware client, if used. 40 | /// 41 | public IDictionary HotModuleReplacementClientOptions { get; set; } 42 | 43 | /// 44 | /// Specifies the Webpack configuration file to be used. If not set, defaults to 'webpack.config.js'. 45 | /// 46 | public string ConfigFile { get; set; } 47 | 48 | /// 49 | /// The root path of your project. Webpack runs in this context. 50 | /// 51 | public string ProjectPath { get; set; } 52 | 53 | /// 54 | /// Specifies additional environment variables to be passed to the Node instance hosting 55 | /// the webpack compiler. 56 | /// 57 | public IDictionary EnvironmentVariables { get; set; } 58 | 59 | /// 60 | /// Specifies a value for the "env" parameter to be passed into the Webpack configuration 61 | /// function. The value must be JSON-serializable, and will only be used if the Webpack 62 | /// configuration is exported as a function. 63 | /// 64 | public object EnvParam { get; set; } 65 | } 66 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Common/NodeInteropFactory.cs: -------------------------------------------------------------------------------- 1 | using Jering.Javascript.NodeJS; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace BrunoLau.SpaServices.Common 9 | { 10 | public static class NodeInteropFactory 11 | { 12 | /// 13 | /// Obtains INodeJSService instance from the service provider 14 | /// 15 | /// 16 | /// 17 | public static INodeJSService GetInstance(IServiceProvider serviceProvider) 18 | { 19 | INodeJSService retVal; 20 | try 21 | { 22 | retVal = serviceProvider.GetService(); 23 | } 24 | catch (Exception) 25 | { 26 | retVal = null; 27 | } 28 | 29 | return retVal; 30 | } 31 | 32 | /// 33 | /// Builds new INodeJSService instance independent of the app services container 34 | /// 35 | /// 36 | /// 37 | /// 38 | public static INodeJSService BuildNewInstance(IDictionary environmentVariables, string projectPath) 39 | { 40 | var services = new ServiceCollection(); 41 | services.AddNodeJS(); 42 | 43 | services.Configure(options => 44 | { 45 | if (environmentVariables != null) 46 | options.EnvironmentVariables = environmentVariables; 47 | 48 | if (!string.IsNullOrWhiteSpace(projectPath)) 49 | options.ProjectPath = projectPath; 50 | }); 51 | 52 | 53 | ServiceProvider serviceProvider = services.BuildServiceProvider(); 54 | return serviceProvider.GetRequiredService(); 55 | } 56 | 57 | /// 58 | /// Builds new INodeJSService instance independent of the app services container 59 | /// 60 | /// 61 | /// 62 | public static INodeJSService BuildNewInstance(IServiceProvider serviceProvider) 63 | { 64 | Dictionary environmentVariables = new Dictionary(); 65 | var hostEnv = serviceProvider.GetService(); 66 | if (hostEnv != null) 67 | { 68 | environmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node 69 | } 70 | 71 | var services = new ServiceCollection(); 72 | services.AddNodeJS(); 73 | ServiceProvider innerProvider = services.BuildServiceProvider(); 74 | services.Configure(options => options.EnvironmentVariables = environmentVariables); 75 | return serviceProvider.GetRequiredService(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /TestApp/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | //const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | //const CheckerPlugin = require('ts-loader').CheckerPlugin; 5 | 6 | module.exports = (env) => { 7 | const isDevBuild = (env == null || env.production != true); 8 | const buildMode = (isDevBuild ? 'development' : 'production'); 9 | const bundleOutputDir = (isDevBuild ? './wwwroot/dist-dev' : './wwwroot/dist'); 10 | //const tsNameof = require("ts-nameof"); 11 | console.log('Building for ' + buildMode + ' environment'); 12 | 13 | return [{ 14 | mode: buildMode, 15 | devtool: false, 16 | context: __dirname, 17 | resolve: { extensions: ['.js', '.ts', '.tsx', '.json'] }, 18 | entry: { 'app': './ClientApp/boot.ts' }, 19 | performance: { 20 | hints: false 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.tsx?$/, 26 | include: /ClientApp/, 27 | exclude: [/node_modules/, /wwwroot/], 28 | use: [ 29 | { 30 | loader: 'babel-loader', 31 | options: { 32 | cacheDirectory: false, 33 | plugins: ["@babel/plugin-syntax-dynamic-import"], 34 | presets: ['@vue/babel-preset-jsx'] 35 | } 36 | }, 37 | { 38 | loader: 'ts-loader' 39 | } 40 | ] 41 | }, 42 | //{ 43 | // test: /\.svg$/, 44 | // loader: 'svg-inline-loader' 45 | //} 46 | { test: /\.css$/, use: isDevBuild ? ['style-loader', 'css-loader'] : ['style-loader', 'css-loader'] }, 47 | { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }, 48 | { test: /\.(ttf|woff2|woff|eot)$/, use: 'file-loader' } 49 | ] 50 | }, 51 | output: { 52 | pathinfo: false, 53 | path: path.join(__dirname, bundleOutputDir), 54 | filename: '[name].js', 55 | chunkFilename: isDevBuild ? '[name].js' : 'splitted/[name]-chunk.[chunkhash:8]-[contenthash:6].js', 56 | publicPath: (isDevBuild ? '/dist-dev/' : '/dist/') 57 | }, 58 | optimization: { 59 | minimize: !isDevBuild 60 | }, 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | 'process.env': { 64 | NODE_ENV: JSON.stringify(isDevBuild ? 'development' : 'production') 65 | } 66 | }), 67 | new webpack.DllReferencePlugin({ 68 | context: __dirname, 69 | manifest: require('./wwwroot/dist/vendor-manifest.json') 70 | }) 71 | ].concat(isDevBuild ? [ 72 | // Plugins that apply in development builds only 73 | new webpack.EvalSourceMapDevToolPlugin({ 74 | filename: "[file].map", 75 | fallbackModuleFilenameTemplate: '[absolute-resource-path]', 76 | moduleFilenameTemplate: '[absolute-resource-path]', 77 | }) 78 | ] : [ 79 | // Plugins that apply in production builds only 80 | //new webpack.optimize.UglifyJsPlugin() 81 | //new ExtractTextPlugin({ 82 | // filename: '[name].css', 83 | // allChunks: true, 84 | // ignoreOrder: true 85 | //}) 86 | ]) 87 | }]; 88 | }; 89 | -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Webpack/ConditionalProxyMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace BrunoLau.SpaServices.Webpack 9 | { 10 | /// 11 | /// Based on ProxyMiddleware from https://github.com/aspnet/Proxy/. 12 | /// Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain 13 | /// This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for 14 | /// chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). 15 | /// 16 | internal class ConditionalProxyMiddleware 17 | { 18 | private const int DefaultHttpBufferSize = 4096; 19 | 20 | private readonly HttpClient _httpClient; 21 | private readonly RequestDelegate _next; 22 | private readonly ConditionalProxyMiddlewareOptions _options; 23 | private readonly string _pathPrefix; 24 | private readonly bool _pathPrefixIsRoot; 25 | 26 | public ConditionalProxyMiddleware( 27 | RequestDelegate next, 28 | string pathPrefix, 29 | ConditionalProxyMiddlewareOptions options) 30 | { 31 | if (!pathPrefix.StartsWith("/")) 32 | { 33 | pathPrefix = "/" + pathPrefix; 34 | } 35 | 36 | _next = next; 37 | _pathPrefix = pathPrefix; 38 | _pathPrefixIsRoot = string.Equals(_pathPrefix, "/", StringComparison.Ordinal); 39 | _options = options; 40 | _httpClient = new HttpClient(new HttpClientHandler()); 41 | _httpClient.Timeout = _options.RequestTimeout; 42 | } 43 | 44 | public async Task Invoke(HttpContext context) 45 | { 46 | if (context.Request.Path.StartsWithSegments(_pathPrefix) || _pathPrefixIsRoot) 47 | { 48 | var didProxyRequest = await PerformProxyRequest(context); 49 | if (didProxyRequest) 50 | { 51 | return; 52 | } 53 | } 54 | 55 | // Not a request we can proxy 56 | await _next.Invoke(context); 57 | } 58 | 59 | private async Task PerformProxyRequest(HttpContext context) 60 | { 61 | var requestMessage = new HttpRequestMessage(); 62 | 63 | // Copy the request headers 64 | foreach (var header in context.Request.Headers) 65 | { 66 | if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) 67 | { 68 | requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); 69 | } 70 | } 71 | 72 | requestMessage.Headers.Host = _options.Host + ":" + _options.Port; 73 | var uriString = 74 | $"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.Path}{context.Request.QueryString}"; 75 | requestMessage.RequestUri = new Uri(uriString); 76 | requestMessage.Method = new HttpMethod(context.Request.Method); 77 | 78 | using ( 79 | var responseMessage = await _httpClient.SendAsync( 80 | requestMessage, 81 | HttpCompletionOption.ResponseHeadersRead, 82 | context.RequestAborted)) 83 | { 84 | if (responseMessage.StatusCode == HttpStatusCode.NotFound) 85 | { 86 | // Let some other middleware handle this 87 | return false; 88 | } 89 | 90 | // We can handle this 91 | context.Response.StatusCode = (int) responseMessage.StatusCode; 92 | foreach (var header in responseMessage.Headers) 93 | { 94 | context.Response.Headers[header.Key] = header.Value.ToArray(); 95 | } 96 | 97 | foreach (var header in responseMessage.Content.Headers) 98 | { 99 | context.Response.Headers[header.Key] = header.Value.ToArray(); 100 | } 101 | 102 | // SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response. 103 | context.Response.Headers.Remove("transfer-encoding"); 104 | 105 | using (var responseStream = await responseMessage.Content.ReadAsStreamAsync()) 106 | { 107 | try 108 | { 109 | await responseStream.CopyToAsync(context.Response.Body, DefaultHttpBufferSize, context.RequestAborted); 110 | } 111 | catch (OperationCanceledException) 112 | { 113 | // The CopyToAsync task will be canceled if the client disconnects (e.g., user 114 | // closes or refreshes the browser tab). Don't treat this as an error. 115 | } 116 | } 117 | 118 | return true; 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Prerendering/Prerenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using BrunoLau.SpaServices.Common; 5 | using Jering.Javascript.NodeJS; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Features; 8 | 9 | namespace BrunoLau.SpaServices.Prerendering 10 | { 11 | /// 12 | /// Performs server-side prerendering by invoking code in Node.js. 13 | /// 14 | public static class Prerenderer 15 | { 16 | private static readonly object CreateNodeScriptLock = new object(); 17 | 18 | private static string NodeScript; 19 | 20 | public static Task RenderToString( 21 | string applicationBasePath, 22 | INodeJSService nodeServices, 23 | CancellationToken applicationStoppingToken, 24 | JavaScriptModuleExport bootModule, 25 | HttpContext httpContext, 26 | object customDataParameter, 27 | int timeoutMilliseconds) 28 | { 29 | // We want to pass the original, unencoded incoming URL data through to Node, so that 30 | // server-side code has the same view of the URL as client-side code (on the client, 31 | // location.pathname returns an unencoded string). 32 | // The following logic handles special characters in URL paths in the same way that 33 | // Node and client-side JS does. For example, the path "/a=b%20c" gets passed through 34 | // unchanged (whereas other .NET APIs do change it - Path.Value will return it as 35 | // "/a=b c" and Path.ToString() will return it as "/a%3db%20c") 36 | var requestFeature = httpContext.Features.Get(); 37 | var unencodedPathAndQuery = requestFeature.RawTarget; 38 | 39 | var request = httpContext.Request; 40 | var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}"; 41 | 42 | return RenderToString( 43 | applicationBasePath, 44 | nodeServices, 45 | applicationStoppingToken, 46 | bootModule, 47 | unencodedAbsoluteUrl, 48 | unencodedPathAndQuery, 49 | customDataParameter, 50 | timeoutMilliseconds, 51 | request.PathBase.ToString()); 52 | } 53 | 54 | /// 55 | /// Performs server-side prerendering by invoking code in Node.js. 56 | /// 57 | /// The root path to your application. This is used when resolving project-relative paths. 58 | /// The instance of that will be used to invoke JavaScript code. 59 | /// A token that indicates when the host application is stopping. 60 | /// The path to the JavaScript file containing the prerendering logic. 61 | /// The URL of the currently-executing HTTP request. This is supplied to the prerendering code. 62 | /// The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code. 63 | /// An optional JSON-serializable parameter to be supplied to the prerendering code. 64 | /// The maximum duration to wait for prerendering to complete. 65 | /// The PathBase for the currently-executing HTTP request. 66 | /// 67 | public static Task RenderToString( 68 | string applicationBasePath, 69 | INodeJSService nodeServices, 70 | CancellationToken applicationStoppingToken, 71 | JavaScriptModuleExport bootModule, 72 | string requestAbsoluteUrl, 73 | string requestPathAndQuery, 74 | object customDataParameter, 75 | int timeoutMilliseconds, 76 | string requestPathBase) 77 | { 78 | 79 | return nodeServices.InvokeFromStringAsync( 80 | GetNodeScript(applicationStoppingToken), //Embedded JS file 81 | args: new object[] { //Actual request args 82 | applicationBasePath, 83 | bootModule, 84 | requestAbsoluteUrl, 85 | requestPathAndQuery, 86 | customDataParameter, 87 | timeoutMilliseconds, 88 | requestPathBase 89 | } 90 | ); 91 | } 92 | 93 | private static string GetNodeScript(CancellationToken applicationStoppingToken) 94 | { 95 | lock (CreateNodeScriptLock) 96 | { 97 | if (NodeScript == null) 98 | { 99 | NodeScript = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); 100 | } 101 | } 102 | 103 | return NodeScript; 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices.Razor/PrerenderTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 6 | using Microsoft.AspNetCore.Mvc.Rendering; 7 | using Microsoft.AspNetCore.Razor.TagHelpers; 8 | using Microsoft.Extensions.Hosting; 9 | using Jering.Javascript.NodeJS; 10 | using BrunoLau.SpaServices.Common; 11 | 12 | namespace BrunoLau.SpaServices.Prerendering 13 | { 14 | /// 15 | /// A tag helper for prerendering JavaScript applications on the server. 16 | /// 17 | [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] 18 | public class PrerenderTagHelper : TagHelper 19 | { 20 | private const string PrerenderModuleAttributeName = "asp-prerender-module"; 21 | private const string PrerenderExportAttributeName = "asp-prerender-export"; 22 | private const string PrerenderDataAttributeName = "asp-prerender-data"; 23 | private const string PrerenderTimeoutAttributeName = "asp-prerender-timeout"; 24 | private static INodeJSService _fallbackNodeServices; // Used only if no INodeServices was registered with DI 25 | 26 | private readonly string _applicationBasePath; 27 | private readonly CancellationToken _applicationStoppingToken; 28 | private readonly INodeJSService _nodeServices; 29 | 30 | /// 31 | /// Creates a new instance of . 32 | /// 33 | /// The . 34 | public PrerenderTagHelper(IServiceProvider serviceProvider) 35 | { 36 | var hostEnv = (IWebHostEnvironment)serviceProvider.GetService(typeof(IWebHostEnvironment)); 37 | _nodeServices = NodeInteropFactory.GetInstance(serviceProvider) ?? _fallbackNodeServices; 38 | _applicationBasePath = hostEnv.ContentRootPath; 39 | 40 | var applicationLifetime = (IHostApplicationLifetime)serviceProvider.GetService(typeof(IHostApplicationLifetime)); 41 | _applicationStoppingToken = applicationLifetime.ApplicationStopping; 42 | 43 | // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() 44 | // in your startup file, but then again it might be confusing that you don't need to. 45 | if (_nodeServices == null) 46 | { 47 | _nodeServices = _fallbackNodeServices = NodeInteropFactory.BuildNewInstance(serviceProvider); 48 | } 49 | } 50 | 51 | /// 52 | /// Specifies the path to the JavaScript module containing prerendering code. 53 | /// 54 | [HtmlAttributeName(PrerenderModuleAttributeName)] 55 | public string ModuleName { get; set; } 56 | 57 | /// 58 | /// If set, specifies the name of the CommonJS export that is the prerendering function to execute. 59 | /// If not set, the JavaScript module's default CommonJS export must itself be the prerendering function. 60 | /// 61 | [HtmlAttributeName(PrerenderExportAttributeName)] 62 | public string ExportName { get; set; } 63 | 64 | /// 65 | /// An optional JSON-serializable parameter to be supplied to the prerendering code. 66 | /// 67 | [HtmlAttributeName(PrerenderDataAttributeName)] 68 | public object CustomDataParameter { get; set; } 69 | 70 | /// 71 | /// The maximum duration to wait for prerendering to complete. 72 | /// 73 | [HtmlAttributeName(PrerenderTimeoutAttributeName)] 74 | public int TimeoutMillisecondsParameter { get; set; } 75 | 76 | /// 77 | /// The . 78 | /// 79 | [HtmlAttributeNotBound] 80 | [ViewContext] 81 | public ViewContext ViewContext { get; set; } 82 | 83 | /// 84 | /// Executes the tag helper to perform server-side prerendering. 85 | /// 86 | /// The . 87 | /// The . 88 | /// A representing the operation. 89 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 90 | { 91 | var result = await Prerenderer.RenderToString( 92 | _applicationBasePath, 93 | _nodeServices, 94 | _applicationStoppingToken, 95 | new JavaScriptModuleExport(ModuleName) 96 | { 97 | ExportName = ExportName 98 | }, 99 | ViewContext.HttpContext, 100 | CustomDataParameter, 101 | TimeoutMillisecondsParameter); 102 | 103 | if (!string.IsNullOrEmpty(result.RedirectUrl)) 104 | { 105 | // It's a redirection 106 | ViewContext.HttpContext.Response.Redirect(result.RedirectUrl); 107 | return; 108 | } 109 | 110 | if (result.StatusCode.HasValue) 111 | { 112 | ViewContext.HttpContext.Response.StatusCode = result.StatusCode.Value; 113 | } 114 | 115 | // It's some HTML to inject 116 | output.Content.SetHtmlContent(result.Html); 117 | 118 | // Also attach any specified globals to the 'window' object. This is useful for transferring 119 | // general state between server and client. 120 | var globalsScript = result.CreateGlobalsAssignmentScript(); 121 | if (!string.IsNullOrEmpty(globalsScript)) 122 | { 123 | output.PostElement.SetHtmlContent($""); 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Content/Node/prerenderer.js: -------------------------------------------------------------------------------- 1 | module.exports = (callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds) => { 2 | // This function is invoked by .NET code (via NodeServices). Its job is to hand off execution to the application's 3 | // prerendering boot function. It can operate in two modes: 4 | // [1] Legacy mode 5 | // This is for backward compatibility with projects created with templates older than the generator version 0.6.0. 6 | // In this mode, we don't really do anything here - we just load the 'aspnet-prerendering' NPM module (which must 7 | // exist in node_modules, and must be v1.x (not v2+)), and pass through all the parameters to it. Code in 8 | // 'aspnet-prerendering' v1.x will locate the boot function and invoke it. 9 | // The drawback to this mode is that, for it to work, you have to deploy node_modules to production. 10 | // [2] Current mode 11 | // This is for projects created with the Yeoman generator 0.6.0+ (or projects manually updated). In this mode, 12 | // we don't invoke 'require' at runtime at all. All our dependencies are bundled into the NuGet package, so you 13 | // don't have to deploy node_modules to production. 14 | // To determine whether we're in mode [1] or [2], the code locates your prerendering boot function, and checks whether 15 | // a certain flag is attached to the function instance. 16 | var renderToStringImpl = function (callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds) { 17 | try { 18 | var forceLegacy = isLegacyAspNetPrerendering(); 19 | var renderToStringFunc = !forceLegacy && findRenderToStringFunc(applicationBasePath, bootModule); 20 | var isNotLegacyMode = renderToStringFunc && renderToStringFunc['isServerRenderer']; 21 | if (isNotLegacyMode) { 22 | // Current (non-legacy) mode - we invoke the exported function directly (instead of going through aspnet-prerendering) 23 | // It's type-safe to just apply the incoming args to this function, because we already type-checked that it's a RenderToStringFunc, 24 | // just like renderToStringImpl itself is. 25 | renderToStringFunc.apply(null, arguments); 26 | } 27 | else { 28 | // Legacy mode - just hand off execution to 'aspnet-prerendering' v1.x, which must exist in node_modules at runtime 29 | var aspNetPrerenderingV1RenderToString = __webpack_require__(3).renderToString; 30 | if (aspNetPrerenderingV1RenderToString) { 31 | aspNetPrerenderingV1RenderToString(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds); 32 | } 33 | else { 34 | callback('If you use aspnet-prerendering >= 2.0.0, you must update your server-side boot module to call createServerRenderer. ' 35 | + 'Either update your boot module code, or revert to aspnet-prerendering version 1.x'); 36 | } 37 | } 38 | } 39 | catch (ex) { 40 | // Make sure loading errors are reported back to the .NET part of the app 41 | callback('Prerendering failed because of error: ' 42 | + ex.stack 43 | + '\nCurrent directory is: ' 44 | + process.cwd()); 45 | } 46 | }; 47 | 48 | var findBootModule = function (applicationBasePath, bootModule) { 49 | var bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); 50 | if (bootModule.webpackConfig) { 51 | // If you're using asp-prerender-webpack-config, you're definitely in legacy mode 52 | return null; 53 | } 54 | else { 55 | return require(bootModuleNameFullPath); 56 | } 57 | } 58 | 59 | var findRenderToStringFunc = function (applicationBasePath, bootModule) { 60 | // First try to load the module 61 | var foundBootModule = findBootModule(applicationBasePath, bootModule); 62 | if (foundBootModule === null) { 63 | return null; // Must be legacy mode 64 | } 65 | // Now try to pick out the function they want us to invoke 66 | var renderToStringFunc; 67 | if (bootModule.exportName) { 68 | // Explicitly-named export 69 | renderToStringFunc = foundBootModule[bootModule.exportName]; 70 | } 71 | else if (typeof foundBootModule !== 'function') { 72 | // TypeScript-style default export 73 | renderToStringFunc = foundBootModule["default"]; 74 | } 75 | else { 76 | // Native default export 77 | renderToStringFunc = foundBootModule; 78 | } 79 | // Validate the result 80 | if (typeof renderToStringFunc !== 'function') { 81 | if (bootModule.exportName) { 82 | throw new Error("The module at " + bootModule.moduleName + " has no function export named " + bootModule.exportName + "."); 83 | } 84 | else { 85 | throw new Error("The module at " + bootModule.moduleName + " does not export a default function, and you have not specified which export to invoke."); 86 | } 87 | } 88 | return renderToStringFunc; 89 | } 90 | 91 | var isLegacyAspNetPrerendering = function () { 92 | var version = getAspNetPrerenderingPackageVersion(); 93 | return version && /^1\./.test(version); 94 | } 95 | 96 | var getAspNetPrerenderingPackageVersion = function () { 97 | try { 98 | var packageEntryPoint = require.resolve('aspnet-prerendering'); 99 | var packageDir = path.dirname(packageEntryPoint); 100 | var packageJsonPath = path.join(packageDir, 'package.json'); 101 | var packageJson = require(packageJsonPath); 102 | return packageJson.version.toString(); 103 | } 104 | catch (ex) { 105 | // Implies aspnet-prerendering isn't in node_modules at all (or node_modules itself doesn't exist, 106 | // which will be the case in production based on latest templates). 107 | return null; 108 | } 109 | } 110 | 111 | renderToStringImpl(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds); 112 | } -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Webpack/WebpackDevMiddleware.cs: -------------------------------------------------------------------------------- 1 | using BrunoLau.SpaServices.Common; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Text.Json; 9 | using System.Threading; 10 | 11 | namespace BrunoLau.SpaServices.Webpack 12 | { 13 | /// 14 | /// Extension methods that can be used to enable Webpack dev middleware support. 15 | /// 16 | public static class WebpackDevMiddleware 17 | { 18 | private const string DefaultConfigFile = "webpack.config.js"; 19 | 20 | private static readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions 21 | { 22 | // Note that the aspnet-webpack JS code specifically expects options to be serialized with 23 | // PascalCase property names, so it's important to be explicit about this contract resolver 24 | PropertyNamingPolicy = null, 25 | 26 | // No need for indentation 27 | WriteIndented = false 28 | }; 29 | 30 | /// 31 | /// Enables Webpack dev middleware support. This hosts an instance of the Webpack compiler in memory 32 | /// in your application so that you can always serve up-to-date Webpack-built resources without having 33 | /// to run the compiler manually. Since the Webpack compiler instance is retained in memory, incremental 34 | /// compilation is vastly faster that re-running the compiler from scratch. 35 | /// 36 | /// Incoming requests that match Webpack-built files will be handled by returning the Webpack compiler 37 | /// output directly, regardless of files on disk. If compilation is in progress when the request arrives, 38 | /// the response will pause until updated compiler output is ready. 39 | /// 40 | /// The . 41 | /// Options for configuring the Webpack compiler instance. 42 | public static void UseWebpackDevMiddlewareEx( 43 | this IApplicationBuilder appBuilder, 44 | WebpackDevMiddlewareOptions options = null) 45 | { 46 | // Prepare options 47 | if (options == null) 48 | { 49 | options = new WebpackDevMiddlewareOptions(); 50 | } 51 | 52 | // Validate options 53 | if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) 54 | { 55 | throw new ArgumentException( 56 | "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); 57 | } 58 | 59 | //Determine project path and environment variables 60 | string projectPath; 61 | Dictionary environmentVariables = new Dictionary(); 62 | if (!string.IsNullOrEmpty(options.ProjectPath)) 63 | { 64 | projectPath = options.ProjectPath; 65 | } 66 | else 67 | { 68 | var hostEnv = appBuilder.ApplicationServices.GetService(); 69 | if (hostEnv != null) 70 | { 71 | // In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few 72 | // things that you'd otherwise have to specify manually 73 | projectPath = hostEnv.ContentRootPath; 74 | environmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node 75 | } 76 | else 77 | { 78 | projectPath = Directory.GetCurrentDirectory(); 79 | } 80 | } 81 | 82 | if (options.EnvironmentVariables != null) 83 | { 84 | foreach (var kvp in options.EnvironmentVariables) 85 | { 86 | environmentVariables[kvp.Key] = kvp.Value; 87 | } 88 | } 89 | 90 | // Ideally, this would be relative to the application's PathBase (so it could work in virtual directories) 91 | // but it's not clear that such information exists during application startup, as opposed to within the context 92 | // of a request. 93 | var hmrEndpoint = !string.IsNullOrEmpty(options.HotModuleReplacementEndpoint) 94 | ? options.HotModuleReplacementEndpoint 95 | : "/__webpack_hmr"; // Matches webpack's built-in default 96 | 97 | // Tell Node to start the server hosting webpack-dev-middleware 98 | var devServerOptions = new WebpackDevServerArgs 99 | { 100 | webpackConfigPath = Path.Combine(projectPath, options.ConfigFile ?? DefaultConfigFile), 101 | suppliedOptions = options, 102 | understandsMultiplePublicPaths = true, 103 | hotModuleReplacementEndpointUrl = hmrEndpoint 104 | }; 105 | 106 | // Perform the webpack-hot-middleware package patch so taht overlay works, until fixed by the package owner 107 | if (options.TryPatchHotModulePackage) 108 | { 109 | PatchHotModuleMiddleware(projectPath); 110 | } 111 | 112 | // Launch the dev server by using Node interop with hack that fixes aspnet-webpack module to work wil Webpack 5 + webpack-dev-middleware 5 113 | var devServerInfo = StartWebpackDevServer(environmentVariables, options.ProjectPath, devServerOptions, false); 114 | 115 | // If we're talking to an older version of aspnet-webpack, it will return only a single PublicPath, 116 | // not an array of PublicPaths. Handle that scenario. 117 | if (devServerInfo.PublicPaths == null) 118 | { 119 | devServerInfo.PublicPaths = new[] { devServerInfo.PublicPath }; 120 | } 121 | 122 | // Proxy the corresponding requests through ASP.NET and into the Node listener 123 | // Anything under / (e.g., /dist) is proxied as a normal HTTP request with a typical timeout (100s is the default from HttpClient), 124 | // plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request). 125 | foreach (var publicPath in devServerInfo.PublicPaths) 126 | { 127 | appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath + hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan); 128 | appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100)); 129 | } 130 | } 131 | 132 | /// 133 | /// Starts the webpack dev server. If the start fails for known reason, modifies the aspnet-webpack module to be compliant with webpack-dev-middleware 5. 134 | /// For compatibility purposes as the change is rather samll it's easier to modify the existing module than to create new NPM package and enforce anyone to udpate. 135 | /// 136 | private static WebpackDevServerInfo StartWebpackDevServer(IDictionary environmentVariables, string projectPath, WebpackDevServerArgs devServerArgs, bool fixAttempted) 137 | { 138 | // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it 139 | // use your DI configuration. It's important for WebpackDevMiddleware to have its own private Node instance 140 | // because it must *not* restart when files change (if it did, you'd lose all the benefits of Webpack 141 | // middleware). And since this is a dev-time-only feature, it doesn't matter if the default transport isn't 142 | // as fast as some theoretical future alternative. 143 | // This should do it by using Jering.Javascript.NodeJS interop 144 | var nodeJSService = NodeInteropFactory.BuildNewInstance(environmentVariables, projectPath); 145 | 146 | try 147 | { 148 | return nodeJSService.InvokeFromStringAsync( 149 | EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), "/Content/Node/webpack-dev-middleware.js"), //Embedded JS file 150 | args: new object[] { JsonSerializer.Serialize(devServerArgs, jsonSerializerOptions) } //Options patched so that they work with aspnet-webpack package 151 | ).Result; 152 | } 153 | catch (Exception ex) 154 | { 155 | if (fixAttempted) 156 | { 157 | throw; 158 | } 159 | 160 | if (ex != null && ex.Message.Contains("Dev Middleware has been initialized using an options object that does not match the API schema.")) 161 | { 162 | //Attempt to modify module file so that it doesn't contain arguments not recognized by the webpack-dev-middleware 5 163 | try 164 | { 165 | const string SEARCH_PATTERN = "at validate ("; 166 | var startIndex = ex.Message.IndexOf(SEARCH_PATTERN); 167 | if (startIndex > -1) 168 | { 169 | startIndex += SEARCH_PATTERN.Length; 170 | var endIndex = ex.Message.IndexOf("webpack-dev-middleware", startIndex); 171 | var modulesPath = ex.Message.Substring(startIndex, endIndex - startIndex); 172 | 173 | if (Directory.Exists(modulesPath)) 174 | { 175 | var modulePath = Path.Combine(modulesPath, @"aspnet-webpack\WebpackDevMiddleware.js"); 176 | if (File.Exists(modulePath)) 177 | { 178 | var fileContent = File.ReadAllText(modulePath); 179 | fileContent = fileContent.Replace("noInfo: true,", ""); 180 | fileContent = fileContent.Replace("watchOptions: webpackConfig.watchOptions", ""); 181 | File.WriteAllText(modulePath, fileContent); 182 | nodeJSService.Dispose(); 183 | 184 | return StartWebpackDevServer(environmentVariables, projectPath, devServerArgs, true); 185 | } 186 | } 187 | } 188 | } 189 | catch (Exception) 190 | { } 191 | } 192 | 193 | throw; 194 | } 195 | } 196 | 197 | /// 198 | /// Attempts to patch the webpack-hot-middleware so that it works with Webpack 5 199 | /// 200 | /// 201 | private static void PatchHotModuleMiddleware(string projectPath) 202 | { 203 | var hotModuleDir = Path.Combine(projectPath, @"node_modules\webpack-hot-middleware"); 204 | if (Directory.Exists(hotModuleDir)) 205 | { 206 | var pathDat = Path.Combine(hotModuleDir, "patchDone.dat"); 207 | if (File.Exists(pathDat)) 208 | { 209 | return; 210 | } 211 | 212 | var middlewarePath = Path.Combine(hotModuleDir, "middleware.js"); 213 | if (File.Exists(middlewarePath)) 214 | { 215 | var middlewareContent = File.ReadAllText(middlewarePath); 216 | if (!middlewareContent.Contains("//patched by the init script")) 217 | { 218 | middlewareContent = middlewareContent.Replace("statsResult.toJson({", "statsResult.toJson({errors:true,warnings:true,"); 219 | middlewareContent = middlewareContent.Replace("function publishStats(action", @"function formatErrors(n){return n&&n.length?'string'==typeof n[0]?n:n.map(function(n){return n.moduleName+' '+n.loc+'\n'+n.message}):[]} function publishStats(action"); 220 | middlewareContent = middlewareContent.Replace("stats.warnings || []", "formatErrors(stats.warnings), //patched by the init script"); 221 | middlewareContent = middlewareContent.Replace("stats.errors || []", "formatErrors(stats.errors), //patched by the init script"); 222 | File.WriteAllText(middlewarePath, middlewareContent); 223 | File.WriteAllText(pathDat, "ok"); 224 | } 225 | } 226 | } 227 | } 228 | 229 | private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout) 230 | { 231 | // Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the 232 | // server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is 233 | // the one making the internal HTTP requests, and it's going to be to some port on this machine 234 | // because aspnet-webpack hosts the dev server there. We can't use the hostname that the client 235 | // sees, because that could be anything (e.g., some upstream load balancer) and we might not be 236 | // able to make outbound requests to it from here. 237 | // Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS, 238 | // because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic 239 | // to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have 240 | // the necessary certificate). 241 | var proxyOptions = new ConditionalProxyMiddlewareOptions( 242 | "http", "localhost", proxyToPort.ToString(), requestTimeout); 243 | appBuilder.UseMiddleware(publicPath, proxyOptions); 244 | } 245 | 246 | private class WebpackDevServerArgs 247 | { 248 | public string webpackConfigPath { get; set; } 249 | public WebpackDevMiddlewareOptions suppliedOptions { get; set; } 250 | public bool understandsMultiplePublicPaths { get; set; } 251 | public string hotModuleReplacementEndpointUrl { get; set; } 252 | 253 | } 254 | 255 | #pragma warning disable CS0649 256 | class WebpackDevServerInfo 257 | { 258 | public int Port { get; set; } 259 | public string[] PublicPaths { get; set; } 260 | 261 | // For back-compatibility with older versions of aspnet-webpack, in the case where your webpack 262 | // configuration contains exactly one config entry. This will be removed soon. 263 | public string PublicPath { get; set; } 264 | } 265 | } 266 | #pragma warning restore CS0649 267 | } -------------------------------------------------------------------------------- /TestApp/wwwroot/dist/app.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see app.js.LICENSE.txt */ 2 | (()=>{var t={332:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>s});var r=n(81),o=n.n(r),i=n(645),a=n.n(i)()(o());a.push([t.id,".main-nav li .glyphicon {\n margin-right: 10px;\n}\n\n/* Highlighting rules for nav menu items */\n.main-nav li a.router-link-active,\n.main-nav li a.router-link-active:hover,\n.main-nav li a.router-link-active:focus {\n background-color: #4189C7;\n color: white;\n}\n\n/* Keep the nav menu independent of scrolling and on top of other items */\n.main-nav {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n z-index: 1;\n}\n\n@media (max-width: 767px) {\n /* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */\n body {\n padding-top: 50px;\n }\n}\n\n@media (min-width: 768px) {\n /* On small screens, convert the nav menu to a vertical sidebar */\n .main-nav {\n height: 100%;\n width: calc(25% - 20px);\n }\n .main-nav .navbar {\n border-radius: 0px;\n border-width: 0px;\n height: 100%;\n }\n .main-nav .navbar-header {\n float: none;\n }\n .main-nav .navbar-collapse {\n border-top: 1px solid #444;\n padding: 0px;\n }\n .main-nav .navbar ul {\n float: none;\n }\n .main-nav .navbar li {\n float: none;\n font-size: 15px;\n margin: 6px;\n }\n .main-nav .navbar li a {\n padding: 10px 16px;\n border-radius: 4px;\n }\n .main-nav .navbar a {\n /* If a menu item's text is too long, truncate it */\n width: 100%;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n}\n",""]);const s=a},645:t=>{"use strict";t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var n="",r=void 0!==e[5];return e[4]&&(n+="@supports (".concat(e[4],") {")),e[2]&&(n+="@media ".concat(e[2]," {")),r&&(n+="@layer".concat(e[5].length>0?" ".concat(e[5]):""," {")),n+=t(e),r&&(n+="}"),e[2]&&(n+="}"),e[4]&&(n+="}"),n})).join("")},e.i=function(t,n,r,o,i){"string"==typeof t&&(t=[[null,t,void 0]]);var a={};if(r)for(var s=0;s0?" ".concat(f[5]):""," {").concat(f[1],"}")),f[5]=i),n&&(f[2]?(f[1]="@media ".concat(f[2]," {").concat(f[1],"}"),f[2]=n):f[2]=n),o&&(f[4]?(f[1]="@supports (".concat(f[4],") {").concat(f[1],"}"),f[4]=o):f[4]="".concat(o)),e.push(f))}},e}},81:t=>{"use strict";t.exports=function(t){return t[1]}},702:function(t,e,n){t.exports=function(){"use strict";function t(t){return"function"==typeof t}var e=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},r=0,o=void 0,i=void 0,a=function(t,e){d[r]=t,d[r+1]=e,2===(r+=2)&&(i?i(h):g())};var s="undefined"!=typeof window?window:void 0,c=s||{},u=c.MutationObserver||c.WebKitMutationObserver,f="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),l="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel;function p(){var t=setTimeout;return function(){return t(h,1)}}var d=new Array(1e3);function h(){for(var t=0;t{var r=n(332);"string"==typeof r&&(r=[[t.id,r,""]]);n(723)(r,{hmr:!0,transform:void 0,insertInto:void 0}),r.locals&&(t.exports=r.locals)},723:(t,e,n)=>{var r,o,i={},a=(r=function(){return window&&document&&document.all&&!window.atob},function(){return void 0===o&&(o=r.apply(this,arguments)),o}),s=function(t,e){return e?e.querySelector(t):document.querySelector(t)},c=function(t){var e={};return function(t,n){if("function"==typeof t)return t();if(void 0===e[t]){var r=s.call(this,t,n);if(window.HTMLIFrameElement&&r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(t){r=null}e[t]=r}return e[t]}}(),u=null,f=0,l=[],p=n(947);function d(t,e){for(var n=0;n=0&&l.splice(e,1)}function b(t){var e=document.createElement("style");if(void 0===t.attrs.type&&(t.attrs.type="text/css"),void 0===t.attrs.nonce){var r=n.nc;r&&(t.attrs.nonce=r)}return m(e,t.attrs),v(t,e),e}function m(t,e){Object.keys(e).forEach((function(n){t.setAttribute(n,e[n])}))}function g(t,e){var n,r,o,i;if(e.transform&&t.css){if(!(i="function"==typeof e.transform?e.transform(t.css):e.transform.default(t.css)))return function(){};t.css=i}if(e.singleton){var a=f++;n=u||(u=b(e)),r=O.bind(null,n,a,!1),o=O.bind(null,n,a,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(t){var e=document.createElement("link");return void 0===t.attrs.type&&(t.attrs.type="text/css"),t.attrs.rel="stylesheet",m(e,t.attrs),v(t,e),e}(e),r=x.bind(null,n,e),o=function(){y(n),n.href&&URL.revokeObjectURL(n.href)}):(n=b(e),r=j.bind(null,n),o=function(){y(n)});return r(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;r(t=e)}else o()}}t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=a()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var n=h(t,e);return d(n,e),function(t){for(var r=[],o=0;o{t.exports=function(t){var e="undefined"!=typeof window&&window.location;if(!e)throw new Error("fixUrls requires window.location");if(!t||"string"!=typeof t)return t;var n=e.protocol+"//"+e.host,r=n+e.pathname.replace(/\/[^\/]*$/,"/");return t.replace(/url\s*\(((?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)\)/gi,(function(t,e){var o,i=e.trim().replace(/^"(.*)"$/,(function(t,e){return e})).replace(/^'(.*)'$/,(function(t,e){return e}));return/^(#|data:|http:\/\/|https:\/\/|file:\/\/\/|\s*$)/i.test(i)?t:(o=0===i.indexOf("//")?i:0===i.indexOf("/")?n+i:r+i.replace(/^\.\//,""),"url("+JSON.stringify(o)+")")}))}},788:(t,e,n)=>{t.exports=n(99)(144)},671:(t,e,n)=>{t.exports=n(99)(345)},254:(t,e,n)=>{t.exports=n(99)(734)},99:t=>{"use strict";t.exports=vendor_f22b82494aa4bc4a64cf},327:()=>{}},e={};function n(r){var o=e[r];if(void 0!==o)return o.exports;var i=e[r]={id:r,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},(()=>{"use strict";n(254);var t=n(788),e=n(671);function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(){return"undefined"!=typeof Reflect&&Reflect.defineMetadata&&Reflect.getOwnMetadataKeys}function a(t,e){s(t,e),Object.getOwnPropertyNames(e.prototype).forEach((function(n){s(t.prototype,e.prototype,n)})),Object.getOwnPropertyNames(e).forEach((function(n){s(t,e,n)}))}function s(t,e,n){(n?Reflect.getOwnMetadataKeys(e,n):Reflect.getOwnMetadataKeys(e)).forEach((function(r){var o=n?Reflect.getOwnMetadata(r,e,n):Reflect.getOwnMetadata(r,e);n?Reflect.defineMetadata(r,o,t,n):Reflect.defineMetadata(r,o,t)}))}var c={__proto__:[]}instanceof Array;function u(t,e){var n=e.prototype._init;e.prototype._init=function(){var e=this,n=Object.getOwnPropertyNames(t);if(t.$options.props)for(var r in t.$options.props)t.hasOwnProperty(r)||n.push(r);n.forEach((function(n){Object.defineProperty(e,n,{get:function(){return t[n]},set:function(e){t[n]=e},configurable:!0})}))};var r=new e;e.prototype._init=n;var o={};return Object.keys(r).forEach((function(t){void 0!==r[t]&&(o[t]=r[t])})),o}var f=["data","beforeCreate","created","beforeMount","mounted","beforeDestroy","destroyed","beforeUpdate","updated","activated","deactivated","render","errorCaptured","serverPrefetch"];function l(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};n.name=n.name||e._componentTag||e.name;var r=e.prototype;Object.getOwnPropertyNames(r).forEach((function(t){if("constructor"!==t)if(f.indexOf(t)>-1)n[t]=r[t];else{var e=Object.getOwnPropertyDescriptor(r,t);void 0!==e.value?"function"==typeof e.value?(n.methods||(n.methods={}))[t]=e.value:(n.mixins||(n.mixins=[])).push({data:function(){return o({},t,e.value)}}):(e.get||e.set)&&((n.computed||(n.computed={}))[t]={get:e.get,set:e.set})}})),(n.mixins||(n.mixins=[])).push({data:function(){return u(this,e)}});var s=e.__decorators__;s&&(s.forEach((function(t){return t(n)})),delete e.__decorators__);var c=Object.getPrototypeOf(e.prototype),l=c instanceof t.default?c.constructor:t.default,p=l.extend(n);return d(p,e,l),i()&&a(p,e),p}var p={prototype:!0,arguments:!0,callee:!0,caller:!0};function d(t,e,n){Object.getOwnPropertyNames(e).forEach((function(o){if(!p[o]){var i=Object.getOwnPropertyDescriptor(t,o);if(!i||i.configurable){var a,s,u=Object.getOwnPropertyDescriptor(e,o);if(!c){if("cid"===o)return;var f=Object.getOwnPropertyDescriptor(n,o);if(s=r(a=u.value),null!=a&&("object"===s||"function"===s)&&f&&f.value===u.value)return}Object.defineProperty(t,o,u)}}}))}function h(t){return"function"==typeof t?l(t):function(e){return l(e,t)}}h.registerHooks=function(t){var e;f.push.apply(f,function(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e=0;s--)(o=t[s])&&(a=(i<3?o(a):i>3?o(e,n,a):o(e,n))||a);return i>3&&a&&Object.defineProperty(e,n,a),a}([v],e)}(t.default);var g=function(){var t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])},t(e,n)};return function(e,n){function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();const w=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return g(e,t),e.prototype.render=function(t){return t("div",{attrs:{id:"app-root"},class:"container-fluid"},[t(m),t("router-view")])},function(t,e,n,r){var o,i=arguments.length,a=i<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,n,r);else for(var s=t.length-1;s>=0;s--)(o=t[s])&&(a=(i<3?o(a):i>3?o(e,n,a):o(e,n))||a);return i>3&&a&&Object.defineProperty(e,n,a),a}([v],e)}(t.default);var _=function(){var t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])},t(e,n)};return function(e,n){function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();const O=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.currentcount=5,e}return _(e,t),e.prototype.incrementCounter=function(){this.currentcount++},e.prototype.render=function(t){var e=this;return t("div",[t("h1",["Counter TSX"]),t("p",["Counter done the TSX + Vue.js 2 way"]),t("p",["Current count: ",t("strong",[this.currentcount])]),t("button",{on:{click:function(){e.incrementCounter()}}},["Increment"])])},function(t,e,n,r){var o,i=arguments.length,a=i<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,n,r);else for(var s=t.length-1;s>=0;s--)(o=t[s])&&(a=(i<3?o(a):i>3?o(e,n,a):o(e,n))||a);return i>3&&a&&Object.defineProperty(e,n,a),a}([v],e)}(t.default);var j=function(){var t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])},t(e,n)};return function(e,n){function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();const x=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return j(e,t),e.prototype.render=function(t){return t("div",[t("h1",["Hello, world!"]),t("p",["Welcome to your new single-page application, built with:"]),t("ul",[t("li",[t("a",{attrs:{href:"https://get.asp.net/"}},["ASP.NET Core"])," and ",t("a",{attrs:{href:"https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx"}},["C#"])," for cross-platform server-side code"]),t("li",[t("a",{attrs:{href:"https://vuejs.org/"}},["Vue.js"])," and ",t("a",{attrs:{href:"http://www.typescriptlang.org/"}},["TypeScript"])," for client-side code"]),t("li",[t("a",{attrs:{href:"https://webpack.github.io/"}},["Webpack"])," for building and bundling client-side resources"]),t("li",[t("a",{attrs:{href:"http://getbootstrap.com/"}},["Bootstrap"])," for layout and styling"])]),t("p",["To help you get started, we've also set up:"]),t("ul",[t("li",[t("strong",["Client-side navigation"]),". For example, click ",t("em",["Counter"])," then ",t("em",["Back"])," to return here."]),t("li",[t("strong",["Webpack dev middleware"]),". In development mode, there's no need to run the ",t("code",["webpack"])," build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file."]),t("li",[t("strong",["Hot module replacement"]),". In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Vue app will be rebuilt and a new instance injected is into the page."]),t("li",[t("strong",["Efficient production builds"]),". In production mode, development-time features are disabled, and the ",t("code",["webpack"])," build tool produces minified static CSS and JavaScript files."])])])},function(t,e,n,r){var o,i=arguments.length,a=i<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,n,r);else for(var s=t.length-1;s>=0;s--)(o=t[s])&&(a=(i<3?o(a):i>3?o(e,n,a):o(e,n))||a);return i>3&&a&&Object.defineProperty(e,n,a),a}([v],e)}(t.default);var P=function(){var t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])},t(e,n)};return function(e,n){function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),A=function(t,e,n,r){return new(n||(n=Promise))((function(o,i){function a(t){try{c(r.next(t))}catch(t){i(t)}}function s(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(a,s)}c((r=r.apply(t,e||[])).next())}))},S=function(t,e){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!((o=(o=a.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]=0;s--)(o=t[s])&&(a=(i<3?o(a):i>3?o(e,n,a):o(e,n))||a);return i>3&&a&&Object.defineProperty(e,n,a),a}([v],e)}(t.default);window.Promise||(window.Promise=n(702).Promise),t.default.use(e.default);var T=[{path:"/",component:x},{path:"/counter",component:O},{path:"/fetchdata",component:R}];new t.default({el:"#app-root",router:new e.default({mode:"history",routes:T}),render:function(t){return t(w)}})})()})(); -------------------------------------------------------------------------------- /BrunoLau.SpaServices/Content/Node/webpack-dev-middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = (callback, runArgs) => { 2 | var aspNetWebpack; 3 | var requireNewCopyProvider; 4 | var webpackTestPermissions; 5 | var connect; 6 | var exStack = ''; 7 | 8 | try { 9 | connect = require("connect"); 10 | } catch (ex) { 11 | connect = null; 12 | exStack = ex.stack; 13 | } 14 | 15 | if (connect == null) { 16 | callback('Webpack dev middleware failed because of an error while loading \'connect\' package. Ensure you have the \'connect\' npm package installed. Error was: ' 17 | + exStack 18 | + '\nCurrent directory is: ' 19 | + process.cwd()); 20 | return; 21 | } 22 | 23 | 24 | 25 | (function () { 26 | function requireNewCopy(moduleNameOrPath) { 27 | // Store a reference to whatever's in the 'require' cache, 28 | // so we don't permanently destroy it, and then ensure there's 29 | // no cache entry for this module 30 | var resolvedModule = require.resolve(moduleNameOrPath); 31 | var wasCached = resolvedModule in require.cache; 32 | var cachedInstance; 33 | if (wasCached) { 34 | cachedInstance = require.cache[resolvedModule]; 35 | delete require.cache[resolvedModule]; 36 | } 37 | try { 38 | // Return a new copy 39 | return require(resolvedModule); 40 | } 41 | finally { 42 | // Restore the cached entry, if any 43 | if (wasCached) { 44 | require.cache[resolvedModule] = cachedInstance; 45 | } 46 | } 47 | } 48 | 49 | requireNewCopyProvider = { 50 | requireNewCopy: requireNewCopy 51 | } 52 | })(); 53 | 54 | (function () { 55 | var fs = require("fs"); 56 | var path = require("path"); 57 | var isWindows = /^win/.test(process.platform); 58 | // On Windows, Node (still as of v8.1.3) has an issue whereby, when locating JavaScript modules 59 | // on disk, it walks up the directory hierarchy to the disk root, testing whether each directory 60 | // is a symlink or not. This fails with an exception if the process doesn't have permission to 61 | // read those directories. This is a problem when hosting in full IIS, because in typical cases 62 | // the process does not have read permission for higher-level directories. 63 | // 64 | // NodeServices itself works around this by injecting a patched version of Node's 'lstat' API that 65 | // suppresses these irrelevant errors during module loads. This covers most scenarios, but isn't 66 | // enough to make Webpack dev middleware work, because typical Webpack configs use loaders such as 67 | // 'awesome-typescript-loader', which works by forking a child process to do some of its work. The 68 | // child process does not get the patched 'lstat', and hence fails. It's an especially bad failure, 69 | // because the Webpack compiler doesn't even surface the exception - it just never completes the 70 | // compilation process, causing the application to hang indefinitely. 71 | // 72 | // Additionally, Webpack dev middleware will want to write its output to disk, which is also going 73 | // to fail in a typical IIS process, because you won't have 'write' permission to the app dir by 74 | // default. We have to actually write the build output to disk (and not purely keep it in the in- 75 | // memory file system) because the server-side prerendering Node instance is a separate process 76 | // that only knows about code changes when it sees the compiled files on disk change. 77 | // 78 | // In the future, we'll hopefully get Node to fix its underlying issue, and figure out whether VS 79 | // could give 'write' access to the app dir when launching sites in IIS. But until then, disable 80 | // Webpack dev middleware if we detect the server process doesn't have the necessary permissions. 81 | function hasSufficientPermissions() { 82 | if (isWindows) { 83 | return canReadDirectoryAndAllAncestors(process.cwd()); 84 | } 85 | else { 86 | return true; 87 | } 88 | } 89 | function canReadDirectoryAndAllAncestors(dir) { 90 | if (!canReadDirectory(dir)) { 91 | return false; 92 | } 93 | var parentDir = path.resolve(dir, '..'); 94 | if (parentDir === dir) { 95 | // There are no more parent directories - we've reached the disk root 96 | return true; 97 | } 98 | else { 99 | return canReadDirectoryAndAllAncestors(parentDir); 100 | } 101 | } 102 | function canReadDirectory(dir) { 103 | try { 104 | fs.statSync(dir); 105 | return true; 106 | } 107 | catch (ex) { 108 | return false; 109 | } 110 | } 111 | 112 | webpackTestPermissions = { 113 | hasSufficientPermissions: hasSufficientPermissions 114 | } 115 | 116 | })(); 117 | 118 | (function () { 119 | var webpack = require("webpack"); 120 | var fs = require("fs"); 121 | var path = require("path"); 122 | var querystring = require("querystring"); 123 | function isThenable(obj) { 124 | return obj && typeof obj.then === 'function'; 125 | } 126 | function attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientOptions, hmrServerEndpoint) { 127 | // Build the final Webpack config based on supplied options 128 | if (enableHotModuleReplacement) { 129 | // For this, we only support the key/value config format, not string or string[], since 130 | // those ones don't clearly indicate what the resulting bundle name will be 131 | var entryPoints_1 = webpackConfig.entry; 132 | var isObjectStyleConfig = entryPoints_1 133 | && typeof entryPoints_1 === 'object' 134 | && !(entryPoints_1 instanceof Array); 135 | if (!isObjectStyleConfig) { 136 | 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\' }")'); 137 | } 138 | // Augment all entry points so they support HMR (unless they already do) 139 | Object.getOwnPropertyNames(entryPoints_1).forEach(function (entryPointName) { 140 | var webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client'; 141 | var webpackHotMiddlewareOptions = '?' + querystring.stringify(hmrClientOptions); 142 | if (typeof entryPoints_1[entryPointName] === 'string') { 143 | entryPoints_1[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints_1[entryPointName]]; 144 | } 145 | else if (firstIndexOfStringStartingWith(entryPoints_1[entryPointName], webpackHotMiddlewareEntryPoint) < 0) { 146 | entryPoints_1[entryPointName].unshift(webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions); 147 | } 148 | // Now also inject eventsource polyfill so this can work on IE/Edge (unless it's already there) 149 | // To avoid this being a breaking change for everyone who uses aspnet-webpack, we only do this if you've 150 | // referenced event-source-polyfill in your package.json. Note that having event-source-polyfill available 151 | // on the server in node_modules doesn't imply that you've also included it in your client-side bundle, 152 | // but the converse is true (if it's not in node_modules, then you obviously aren't trying to use it at 153 | // all, so it would definitely not work to take a dependency on it). 154 | var eventSourcePolyfillEntryPoint = 'event-source-polyfill'; 155 | if (npmModuleIsPresent(eventSourcePolyfillEntryPoint)) { 156 | var entryPointsArray = entryPoints_1[entryPointName]; // We know by now that it's an array, because if it wasn't, we already wrapped it in one 157 | if (entryPointsArray.indexOf(eventSourcePolyfillEntryPoint) < 0) { 158 | var webpackHmrIndex = firstIndexOfStringStartingWith(entryPointsArray, webpackHotMiddlewareEntryPoint); 159 | if (webpackHmrIndex < 0) { 160 | // This should not be possible, since we just added it if it was missing 161 | throw new Error('Cannot find ' + webpackHotMiddlewareEntryPoint + ' in entry points array: ' + entryPointsArray); 162 | } 163 | // Insert the polyfill just before the HMR entrypoint 164 | entryPointsArray.splice(webpackHmrIndex, 0, eventSourcePolyfillEntryPoint); 165 | } 166 | } 167 | }); 168 | webpackConfig.plugins = [].concat(webpackConfig.plugins || []); // Be sure not to mutate the original array, as it might be shared 169 | webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); 170 | // Set up React HMR support if requested. This requires the 'aspnet-webpack-react' package. 171 | if (enableReactHotModuleReplacement) { 172 | var aspNetWebpackReactModule = void 0; 173 | try { 174 | aspNetWebpackReactModule = require('aspnet-webpack-react'); 175 | } 176 | catch (ex) { 177 | throw new Error('ReactHotModuleReplacement failed because of an error while loading \'aspnet-webpack-react\'. Error was: ' + ex.stack); 178 | } 179 | aspNetWebpackReactModule.addReactHotModuleReplacementBabelTransform(webpackConfig); 180 | } 181 | } 182 | // Attach Webpack dev middleware and optional 'hot' middleware 183 | var compiler = webpack(webpackConfig); 184 | app.use(require('webpack-dev-middleware')(compiler, { 185 | stats: webpackConfig.stats, 186 | publicPath: ensureLeadingSlash(webpackConfig.output.publicPath) 187 | })); 188 | // After each compilation completes, copy the in-memory filesystem to disk. 189 | // This is needed because the debuggers in both VS and VS Code assume that they'll be able to find 190 | // the compiled files on the local disk (though it would be better if they got the source file from 191 | // the browser they are debugging, which would be more correct and make this workaround unnecessary). 192 | // Without this, Webpack plugins like HMR that dynamically modify the compiled output in the dev 193 | // middleware's in-memory filesystem only (and not on disk) would confuse the debugger, because the 194 | // file on disk wouldn't match the file served to the browser, and the source map line numbers wouldn't 195 | // match up. Breakpoints would either not be hit, or would hit the wrong lines. 196 | var copy = function (stats) { return copyRecursiveToRealFsSync(compiler.outputFileSystem, '/', [/\.hot-update\.(js|json|js\.map)$/]); }; 197 | if (compiler.hooks) { 198 | compiler.hooks.done.tap('aspnet-webpack', copy); 199 | } 200 | else { 201 | compiler.plugin('done', copy); 202 | } 203 | if (enableHotModuleReplacement) { 204 | var webpackHotMiddlewareModule = void 0; 205 | try { 206 | webpackHotMiddlewareModule = require('webpack-hot-middleware'); 207 | } 208 | catch (ex) { 209 | throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack); 210 | } 211 | app.use(workaroundIISExpressEventStreamFlushingIssue(hmrServerEndpoint)); 212 | app.use(webpackHotMiddlewareModule(compiler, { 213 | path: hmrServerEndpoint, 214 | overlay: true 215 | })); 216 | } 217 | } 218 | function workaroundIISExpressEventStreamFlushingIssue(path) { 219 | // IIS Express makes HMR seem very slow, because when it's reverse-proxying an EventStream response 220 | // from Kestrel, it doesn't pass through the lines to the browser immediately, even if you're calling 221 | // response.Flush (or equivalent) in your ASP.NET Core code. For some reason, it waits until the following 222 | // line is sent. By default, that wouldn't be until the next HMR heartbeat, which can be up to 5 seconds later. 223 | // In effect, it looks as if your code is taking 5 seconds longer to compile than it really does. 224 | // 225 | // As a workaround, this connect middleware intercepts requests to the HMR endpoint, and modifies the response 226 | // stream so that all EventStream 'data' lines are immediately followed with a further blank line. This is 227 | // harmless in non-IIS-Express cases, because it's OK to have extra blank lines in an EventStream response. 228 | // The implementation is simplistic - rather than using a true stream reader, we just patch the 'write' 229 | // method. This relies on webpack's HMR code always writing complete EventStream messages with a single 230 | // 'write' call. That works fine today, but if webpack's HMR code was changed, this workaround might have 231 | // to be updated. 232 | var eventStreamLineStart = /^data\:/; 233 | return function (req, res, next) { 234 | // We only want to interfere with requests to the HMR endpoint, so check this request matches 235 | var urlMatchesPath = (req.url === path) || (req.url.split('?', 1)[0] === path); 236 | if (urlMatchesPath) { 237 | var origWrite_1 = res.write; 238 | res.write = function (chunk) { 239 | var result = origWrite_1.apply(this, arguments); 240 | // We only want to interfere with actual EventStream data lines, so check it is one 241 | if (typeof (chunk) === 'string') { 242 | if (eventStreamLineStart.test(chunk) && chunk.charAt(chunk.length - 1) === '\n') { 243 | origWrite_1.call(this, '\n\n'); 244 | } 245 | } 246 | return result; 247 | }; 248 | } 249 | return next(); 250 | }; 251 | } 252 | function copyRecursiveToRealFsSync(from, rootDir, exclude) { 253 | from.readdirSync(rootDir).forEach(function (filename) { 254 | var fullPath = pathJoinSafe(rootDir, filename); 255 | var shouldExclude = exclude.filter(function (re) { return re.test(fullPath); }).length > 0; 256 | if (!shouldExclude) { 257 | var fileStat = from.statSync(fullPath); 258 | if (fileStat.isFile()) { 259 | var fileBuf = from.readFileSync(fullPath); 260 | fs.writeFileSync(fullPath, fileBuf); 261 | } 262 | else if (fileStat.isDirectory()) { 263 | if (!fs.existsSync(fullPath)) { 264 | fs.mkdirSync(fullPath); 265 | } 266 | copyRecursiveToRealFsSync(from, fullPath, exclude); 267 | } 268 | } 269 | }); 270 | } 271 | function ensureLeadingSlash(value) { 272 | if (value !== null && value.substring(0, 1) !== '/') { 273 | value = '/' + value; 274 | } 275 | return value; 276 | } 277 | function pathJoinSafe(rootPath, filePath) { 278 | // On Windows, MemoryFileSystem's readdirSync output produces directory entries like 'C:' 279 | // which then trigger errors if you call statSync for them. Avoid this by detecting drive 280 | // names at the root, and adding a backslash (so 'C:' becomes 'C:\', which works). 281 | if (rootPath === '/' && path.sep === '\\' && filePath.match(/^[a-z0-9]+\:$/i)) { 282 | return filePath + '\\'; 283 | } 284 | else { 285 | return path.join(rootPath, filePath); 286 | } 287 | } 288 | function beginWebpackWatcher(webpackConfig) { 289 | var compiler = webpack(webpackConfig); 290 | compiler.watch(webpackConfig.watchOptions || {}, function (err, stats) { 291 | // The default error reporter is fine for now, but could be customized here in the future if desired 292 | }); 293 | } 294 | function createWebpackDevServer(callback, optionsJson) { 295 | var options = JSON.parse(optionsJson); 296 | // Enable TypeScript loading if the webpack config is authored in TypeScript 297 | if (path.extname(options.webpackConfigPath) === '.ts') { 298 | try { 299 | require('ts-node/register'); 300 | } 301 | catch (ex) { 302 | throw new Error('Error while attempting to enable support for Webpack config file written in TypeScript. Make sure your project depends on the "ts-node" NPM package. The underlying error was: ' + ex.stack); 303 | } 304 | } 305 | // See the large comment in WebpackTestPermissions.ts for details about this 306 | if (!webpackTestPermissions.hasSufficientPermissions()) { 307 | console.log('WARNING: Webpack dev middleware is not enabled because the server process does not have sufficient permissions. You should either remove the UseWebpackDevMiddleware call from your code, or to make it work, give your server process user account permission to write to your application directory and to read all ancestor-level directories.'); 308 | callback(null, { 309 | Port: 0, 310 | PublicPaths: [] 311 | }); 312 | return; 313 | } 314 | // Read the webpack config's export, and normalize it into the more general 'array of configs' format 315 | var webpackConfigModuleExports = requireNewCopyProvider.requireNewCopy(options.webpackConfigPath); 316 | var webpackConfigExport = webpackConfigModuleExports.__esModule === true 317 | ? webpackConfigModuleExports.default 318 | : webpackConfigModuleExports; 319 | if (webpackConfigExport instanceof Function) { 320 | // If you export a function, then Webpack convention is that it takes zero or one param, 321 | // and that param is called `env` and reflects the `--env.*` args you can specify on 322 | // the command line (e.g., `--env.prod`). 323 | // When invoking it via WebpackDevMiddleware, we let you configure the `env` param in 324 | // your Startup.cs. 325 | webpackConfigExport = webpackConfigExport(options.suppliedOptions.EnvParam); 326 | } 327 | var webpackConfigThenable = isThenable(webpackConfigExport) 328 | ? webpackConfigExport 329 | : { then: function (callback) { return callback(webpackConfigExport); } }; 330 | webpackConfigThenable.then(function (webpackConfigResolved) { 331 | var webpackConfigArray = webpackConfigResolved instanceof Array ? webpackConfigResolved : [webpackConfigResolved]; 332 | var enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement; 333 | var enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement; 334 | if (enableReactHotModuleReplacement && !enableHotModuleReplacement) { 335 | callback('To use ReactHotModuleReplacement, you must also enable the HotModuleReplacement option.', null); 336 | return; 337 | } 338 | // The default value, 0, means 'choose randomly' 339 | var suggestedHMRPortOrZero = options.suppliedOptions.HotModuleReplacementServerPort || 0; 340 | var app = connect(); 341 | var listener = app.listen(suggestedHMRPortOrZero, function () { 342 | try { 343 | // For each webpack config that specifies a public path, add webpack dev middleware for it 344 | var normalizedPublicPaths_1 = []; 345 | webpackConfigArray.forEach(function (webpackConfig) { 346 | if (webpackConfig.target === 'node') { 347 | // For configs that target Node, it's meaningless to set up an HTTP listener, since 348 | // Node isn't going to load those modules over HTTP anyway. It just loads them directly 349 | // from disk. So the most relevant thing we can do with such configs is just write 350 | // updated builds to disk, just like "webpack --watch". 351 | beginWebpackWatcher(webpackConfig); 352 | } 353 | else { 354 | // For configs that target browsers, we can set up an HTTP listener, and dynamically 355 | // modify the config to enable HMR etc. This just requires that we have a publicPath. 356 | var publicPath = (webpackConfig.output.publicPath || '').trim(); 357 | if (!publicPath) { 358 | throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack config (for any configuration that targets browsers)'); 359 | } 360 | var publicPathNoTrailingSlash = removeTrailingSlash(publicPath); 361 | normalizedPublicPaths_1.push(publicPathNoTrailingSlash); 362 | // This is the URL the client will connect to, except that since it's a relative URL 363 | // (no leading slash), Webpack will resolve it against the runtime URL 364 | // plus it also adds the publicPath 365 | var hmrClientEndpoint = removeLeadingSlash(options.hotModuleReplacementEndpointUrl); 366 | // This is the URL inside the Webpack middleware Node server that we'll proxy to. 367 | // We have to prefix with the public path because Webpack will add the publicPath 368 | // when it resolves hmrClientEndpoint as a relative URL. 369 | var hmrServerEndpoint = ensureLeadingSlash(publicPathNoTrailingSlash + options.hotModuleReplacementEndpointUrl); 370 | // We always overwrite the 'path' option as it needs to match what the .NET side is expecting 371 | var hmrClientOptions = options.suppliedOptions.HotModuleReplacementClientOptions || {}; 372 | hmrClientOptions['path'] = hmrClientEndpoint; 373 | var dynamicPublicPathKey = 'dynamicPublicPath'; 374 | if (!(dynamicPublicPathKey in hmrClientOptions)) { 375 | // dynamicPublicPath default to true, so we can work with nonempty pathbases (virtual directories) 376 | hmrClientOptions[dynamicPublicPathKey] = true; 377 | } 378 | else { 379 | // ... but you can set it to any other value explicitly if you want (e.g., false) 380 | hmrClientOptions[dynamicPublicPathKey] = JSON.parse(hmrClientOptions[dynamicPublicPathKey]); 381 | } 382 | attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientOptions, hmrServerEndpoint); 383 | } 384 | }); 385 | // Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here 386 | callback(null, { 387 | Port: listener.address().port, 388 | PublicPaths: normalizedPublicPaths_1 389 | }); 390 | } 391 | catch (ex) { 392 | callback(ex.stack, null); 393 | } 394 | }); 395 | }, function (err) { return callback(err.stack, null); }); 396 | } 397 | function removeLeadingSlash(str) { 398 | if (str.indexOf('/') === 0) { 399 | str = str.substring(1); 400 | } 401 | return str; 402 | } 403 | function removeTrailingSlash(str) { 404 | if (str.lastIndexOf('/') === str.length - 1) { 405 | str = str.substring(0, str.length - 1); 406 | } 407 | return str; 408 | } 409 | function firstIndexOfStringStartingWith(array, prefixToFind) { 410 | for (var index = 0; index < array.length; index++) { 411 | var candidate = array[index]; 412 | if ((typeof candidate === 'string') && (candidate.substring(0, prefixToFind.length) === prefixToFind)) { 413 | return index; 414 | } 415 | } 416 | return -1; // Not found 417 | } 418 | function npmModuleIsPresent(moduleName) { 419 | try { 420 | require.resolve(moduleName); 421 | return true; 422 | } 423 | catch (ex) { 424 | return false; 425 | } 426 | } 427 | 428 | aspNetWebpack = { 429 | createWebpackDevServer: createWebpackDevServer 430 | } 431 | })(); 432 | 433 | if (aspNetWebpack == null) { 434 | callback('Webpack dev middleware failed because of an error while loading \'aspnet-webpack\'. Error was: ' 435 | + exStack 436 | + '\nCurrent directory is: ' 437 | + process.cwd()); 438 | return; 439 | } 440 | 441 | aspNetWebpack.createWebpackDevServer(callback, runArgs); 442 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # BrunoLau.SpaServices 4 | 5 | [![NuGet Badge](https://buildstats.info/nuget/BrunoLau.SpaServices?includePreReleases=true)](https://www.nuget.org/packages/BrunoLau.SpaServices) 6 | This is a port of deprecated package Microsoft.AspNetCore.SpaServices written by Microsoft. The package aims to bring back features that were removed with the release of .NET 5 - mainly the UseWebpackDevMiddleware extension method. To avoid naming confusions, the extension method has been renamed to UseWebpackDevMiddlewareEx. Migration is simple - where you would use the UseWebpackDevMiddleware() extension method, use the UseWebpackDevMiddlewareEx method. In case you are looking for NodeServices replacement, please take a look at [https://github.com/JeringTech/Javascript.NodeJS](https://github.com/JeringTech/Javascript.NodeJS) package, which this port also uses internally. 7 | 8 | Sample usage: 9 | ``` 10 | app.UseWebpackDevMiddlewareEx(new WebpackDevMiddlewareOptions 11 | { 12 | TryPatchHotModulePackage = true, //Attempts to patch the webpack-hot-middleware module overlay problem 13 | HotModuleReplacement = true 14 | }); 15 | ``` 16 | 17 | See the original Microsoft package documentation below: 18 | 19 | If you're building an ASP.NET Core application, and want to use Angular, React, Knockout, or another single-page app (SPA) framework, this NuGet package contains useful infrastructure for you. 20 | 21 | This package enables: 22 | 23 | * [**Server-side prerendering**](#server-side-prerendering) for *universal* (a.k.a. *isomorphic*) applications, where your Angular / React / etc. components are first rendered on the server, and then transferred to the client where execution continues 24 | * [**Webpack middleware**](#webpack-dev-middleware) so that, during development, any webpack-built resources will be generated on demand, without you having to run webpack manually or compile files to disk 25 | * [**Hot module replacement**](#webpack-hot-module-replacement) so that, during development, your code and markup changes will be pushed to your browser and updated in the running application automatically, without even needing to reload the page 26 | * [**Routing helpers**](#routing-helper-mapspafallbackroute) for integrating server-side routing with client-side routing 27 | 28 | Behind the scenes, it uses the [`Microsoft.AspNetCore.NodeServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices) package as a fast and robust way to invoke Node.js-hosted code from ASP.NET Core at runtime. 29 | 30 | ### Requirements 31 | 32 | * [Node.js](https://nodejs.org/en/) 33 | * To test this is installed and can be found, run `node -v` on a command line 34 | * Note: If you're deploying to an Azure web site, you don't need to do anything here - Node is already installed and available in the server environments 35 | * [.NET Core](https://dot.net), version 1.0 RC2 or later 36 | 37 | ### Installation into existing projects 38 | 39 | * Install the `Microsoft.AspNetCore.SpaServices` NuGet package 40 | * Run `dotnet restore` (or if you use Visual Studio, just wait a moment - it will restore dependencies automatically) 41 | * Install supporting NPM packages for the features you'll be using: 42 | * For **server-side prerendering**, install `aspnet-prerendering` 43 | * For **server-side prerendering with Webpack build support**, also install `aspnet-webpack` 44 | * For **webpack dev middleware**, install `aspnet-webpack` 45 | * For **webpack dev middleware with hot module replacement**, also install `webpack-hot-middleware` 46 | * For **webpack dev middleware with React hot module replacement**, also install `aspnet-webpack-react` 47 | 48 | For example, run `npm install --save aspnet-prerendering aspnet-webpack` to install `aspnet-prerendering` and `aspnet-webpack`. 49 | 50 | 51 | ### Creating entirely new projects 52 | 53 | If you're starting from scratch, you might prefer to use the `aspnetcore-spa` Yeoman generator to get a ready-to-go starting point using your choice of client-side framework. This includes `Microsoft.AspNetCore.SpaServices` along with everything configured for webpack middleware, server-side prerendering, etc. 54 | 55 | See: [Getting started with the aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/) 56 | 57 | Also, if you want to debug projects created with the aspnetcore-spa generator, see [Debugging your projects](#debugging-your-projects) 58 | 59 | ## Server-side prerendering 60 | 61 | The `SpaServices` package isn't tied to any particular client-side framework, and it doesn't force you to set up your client-side application in any one particular style. So, `SpaServices` doesn't contain hard-coded logic for rendering Angular / React / etc. components. 62 | 63 | Instead, what `SpaServices` offers is ASP.NET Core APIs that know how to invoke a JavaScript function that you supply, passing through context information that you'll need for server-side prerendering, and then injects the resulting HTML string into your rendered page. In this document, you'll find examples of setting this up to render Angular and React components. 64 | 65 | ### 1. Enable the asp-prerender-* tag helpers 66 | 67 | Make sure you've installed into your project: 68 | 69 | * The `Microsoft.AspNetCore.SpaServices` NuGet package, version 1.1.0-* or later 70 | * The `aspnet-prerendering` NPM package, version 2.0.1 or later 71 | 72 | Together these contain the server-side and client-side library code you'll need. Now go to your `Views/_ViewImports.cshtml` file, and add the following line: 73 | 74 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices" 75 | 76 | ### 2. Use asp-prerender-* in a view 77 | 78 | Choose a place in one of your MVC views where you want to prerender a SPA component. For example, open `Views/Home/Index.cshtml`, and add markup like the following: 79 | 80 |
81 | 82 | If you run your application now, and browse to whatever page renders the view you just edited, you should get an error similar to the following (assuming you're running in *Development* mode so you can see the error information): *Error: Cannot find module 'some/directory/ClientApp/boot-server'*. You've told the prerendering tag helper to execute code from a JavaScript module called `boot-server`, but haven't yet supplied any such module! 83 | 84 | ### 3. Supplying JavaScript code to perform prerendering 85 | 86 | Create a JavaScript file at the path matching the `asp-prerender-module` value you specified above. In this example, that means creating a folder called `ClientApp` at the root of your project, and creating a file inside it called `boot-server.js`. Try putting the following into it: 87 | 88 | ```javascript 89 | var prerendering = require('aspnet-prerendering'); 90 | 91 | module.exports = prerendering.createServerRenderer(function(params) { 92 | return new Promise(function (resolve, reject) { 93 | var result = '

Hello world!

' 94 | + '

Current time in Node is: ' + new Date() + '

' 95 | + '

Request path is: ' + params.location.path + '

' 96 | + '

Absolute URL is: ' + params.absoluteUrl + '

'; 97 | 98 | resolve({ html: result }); 99 | }); 100 | }); 101 | ``` 102 | 103 | If you try running your app now, you should see the HTML snippet generated by your JavaScript getting injected into your page. 104 | 105 | As you can see, your JavaScript code receives context information (such as the URL being requested), and returns a `Promise` so that it can asynchronously supply the markup to be injected into the page. You can put whatever logic you like here, but typically you'll want to execute a component from your Angular / React / etc. application. 106 | 107 | **Passing data from .NET code into JavaScript code** 108 | 109 | If you want to supply additional data to the JavaScript function that performs your prerendering, you can use the `asp-prerender-data` attribute. You can give any value as long as it's JSON-serializable. Bear in mind that it will be serialized and sent as part of the remote procedure call (RPC) to Node.js, so avoid trying to pass massive amounts of data. 110 | 111 | For example, in your `cshtml`, 112 | 113 |
118 | 119 | Now in your JavaScript prerendering function, you can access this data by reading `params.data`, e.g.: 120 | 121 | ```javascript 122 | var prerendering = require('aspnet-prerendering'); 123 | 124 | module.exports = prerendering.createServerRenderer(function(params) { 125 | return new Promise(function (resolve, reject) { 126 | var result = '

Hello world!

' 127 | + '

Is gold user: ' + params.data.isGoldUser + '

' 128 | + '

Number of cookies: ' + params.data.cookies.length + '

'; 129 | 130 | resolve({ html: result }); 131 | }); 132 | }); 133 | ``` 134 | 135 | Notice that the property names are received in JavaScript-style casing (e.g., `isGoldUser`) even though they were sent in C#-style casing (e.g., `IsGoldUser`). This is because of how the JSON serialization is configured by default. 136 | 137 | **Passing data from server-side to client-side code** 138 | 139 | If, as well as returning HTML, you also want to pass some contextual data from your server-side code to your client-side code, you can supply a `globals` object alongside the initial `html`, e.g.: 140 | 141 | ```javascript 142 | resolve({ 143 | html: result, 144 | globals: { 145 | albumsList: someDataHere, 146 | userData: someMoreDataHere 147 | } 148 | }); 149 | ``` 150 | 151 | When the `aspnet-prerender-*` tag helper emits this result into the document, as well as injecting the `html` string, it will also emit code that populates `window.albumsList` and `window.userData` with JSON-serialized copies of the objects you passed. 152 | 153 | This can be useful if, for example, you want to avoid loading the same data twice (once on the server and once on the client). 154 | 155 | ### 4. Enabling webpack build tooling 156 | 157 | Of course, rather than writing your `boot-server` module and your entire SPA in plain ES5 JavaScript, it's quite likely that you'll want to write your client-side code in TypeScript or at least ES2015 code. To enable this, you need to set up a build system. 158 | 159 | #### Example: Configuring Webpack to build TypeScript 160 | 161 | Let's say you want to write your boot module and SPA code in TypeScript, and build it using Webpack. First ensure that `webpack` is installed, along with the libraries needed for TypeScript compilation: 162 | 163 | npm install -g webpack 164 | npm install --save ts-loader typescript 165 | 166 | Next, create a file `webpack.config.js` at the root of your project, containing: 167 | 168 | ```javascript 169 | var path = require('path'); 170 | 171 | module.exports = { 172 | entry: { 'main-server': './ClientApp/boot-server.ts' }, 173 | resolve: { extensions: [ '', '.js', '.ts' ] }, 174 | output: { 175 | path: path.join(__dirname, './ClientApp/dist'), 176 | filename: '[name].js', 177 | libraryTarget: 'commonjs' 178 | }, 179 | module: { 180 | loaders: [ 181 | { test: /\.ts$/, loader: 'ts-loader' } 182 | ] 183 | }, 184 | target: 'node', 185 | devtool: 'inline-source-map' 186 | }; 187 | ``` 188 | 189 | This tells webpack that it should compile `.ts` files using TypeScript, and that when looking for modules by name (e.g., `boot-server`), it should also find files with `.js` and `.ts` extensions. 190 | 191 | If you don't already have a `tsconfig.json` file at the root of your project, add one now. Make sure your `tsconfig.json` includes `"es6"` in its `"lib"` array so that TypeScript knows about intrinsics such as `Promise`. Here's an example `tsconfig.json`: 192 | 193 | ```json 194 | { 195 | "compilerOptions": { 196 | "moduleResolution": "node", 197 | "target": "es5", 198 | "sourceMap": true, 199 | "lib": [ "es6", "dom" ] 200 | }, 201 | "exclude": [ "bin", "node_modules" ] 202 | } 203 | ``` 204 | 205 | Now you can delete `ClientApp/boot-server.js`, and in its place, create `ClientApp/boot-server.ts`, containing the TypeScript equivalent of what you had before: 206 | 207 | ```javascript 208 | import { createServerRenderer } from 'aspnet-prerendering'; 209 | 210 | export default createServerRenderer(params => { 211 | return new Promise((resolve, reject) => { 212 | const html = ` 213 |

Hello world!

214 |

Current time in Node is: ${ new Date() }

215 |

Request path is: ${ params.location.path }

216 |

Absolute URL is: ${ params.absoluteUrl }

`; 217 | 218 | resolve({ html }); 219 | }); 220 | }); 221 | ``` 222 | 223 | Finally, run `webpack` on the command line to build `ClientApp/dist/main-server.js`. Then you can tell `SpaServices` to use that file for server-side prerendering. In your MVC view where you use `aspnet-prerender-module`, update the attribute value: 224 | 225 |
226 | 227 | Webpack is a broad and powerful tool and can do far more than just invoke the TypeScript compiler. To learn more, see the [webpack website](https://webpack.github.io/). 228 | 229 | 230 | ### 5(a). Prerendering Angular components 231 | 232 | If you're building an Angular application, you can run your components on the server inside your `boot-server.ts` file so they will be injected into the resulting web page. 233 | 234 | First install the NPM package `angular2-universal` - this contains infrastructure for executing Angular components inside Node.js: 235 | 236 | ``` 237 | npm install --save angular2-universal 238 | ``` 239 | 240 | Now you can use the [`angular2-universal` APIs](https://github.com/angular/universal) from your `boot-server.ts` TypeScript module to execute your Angular component on the server. The code needed for this is fairly complex, but that's unavoidable because Angular supports so many different ways of being configured, and you need to provide wiring for whatever combination of DI modules you're using. 241 | 242 | You can find an example `boot-server.ts` that renders arbitrary Angular components [here](../../templates/AngularSpa/ClientApp/boot-server.ts). If you use this with your own application, you might need to edit the `serverBindings` array to reference any other DI services that your Angular component depends on. 243 | 244 | The easiest way to get started with Angular server-side rendering on ASP.NET Core is to use the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/), which creates a ready-made working starting point. 245 | 246 | ### 5(b). Prerendering React components 247 | 248 | React components can be executed synchronously on the server quite easily, although asynchronous execution is tricker as described below. 249 | 250 | #### Setting up client-side React code 251 | 252 | Let's say you want to write a React component in ES2015 code. You might install the NPM modules `react react-dom babel-loader babel-preset-react babel-preset-es2015`, and then prepare Webpack to build `.jsx` files by creating `webpack.config.js` in your project root, containing: 253 | 254 | ```javascript 255 | var path = require('path'); 256 | 257 | module.exports = { 258 | resolve: { extensions: [ '', '.js', '.jsx' ] }, 259 | module: { 260 | loaders: [ 261 | { test: /\.jsx?$/, loader: 'babel-loader' } 262 | ] 263 | }, 264 | entry: { 265 | main: ['./ClientApp/react-app.jsx'], 266 | }, 267 | output: { 268 | path: path.join(__dirname, 'wwwroot', 'dist'), 269 | filename: '[name].js' 270 | }, 271 | }; 272 | ``` 273 | 274 | You will also need a `.babelrc` file in your project root, containing: 275 | 276 | ```javascript 277 | { 278 | "presets": ["es2015", "react"] 279 | } 280 | ``` 281 | 282 | This is enough to be able to build ES2015 `.jsx` files via Webpack. Now you could implement a simple React component, for example the following at `ClientApp/react-app.jsx`: 283 | 284 | ```javascript 285 | import * as React from 'react'; 286 | 287 | export class HelloMessage extends React.Component 288 | { 289 | render() { 290 | return

Hello {this.props.message}!

; 291 | } 292 | } 293 | ``` 294 | 295 | ... and the following code to run it in a browser at `ClientApp/boot-client.jsx`: 296 | 297 | ```javascript 298 | import * as React from 'react'; 299 | import * as ReactDOM from 'react-dom'; 300 | import { HelloMessage } from './react-app'; 301 | 302 | ReactDOM.render(, document.getElementById('my-spa')); 303 | ``` 304 | 305 | At this stage, run `webpack` on the command line to build `wwwroot/dist/main.js`. Or, to avoid having to do this manually, you could use the `SpaServices` package to [enable Webpack dev middleware](#webpack-dev-middleware). 306 | 307 | You can now run your React code on the client by adding the following to one of your MVC views: 308 | 309 |
310 | 311 | 312 | If you want to enable server-side prerendering too, follow the same process as described under [server-side prerendering](#server-side-prerendering). 313 | 314 | #### Realistic React apps and Redux 315 | 316 | The above example is extremely simple - it doesn't use `react-router`, and it doesn't load any data asynchronously. Real applications are likely to do both of these. 317 | 318 | For an example server-side boot module that knows how to evaluate `react-router` routes and render the correct React component, see [this example](../../templates/ReactReduxSpa/ClientApp/boot-server.tsx). 319 | 320 | Supporting asynchronous data loading involves more considerations. Unlike Angular applications that run asynchronously on the server and freely overwrite server-generated markup with client-generated markup, React strictly wants to run synchronously on the server and always produce the same markup on the server as it does on the client. 321 | 322 | To make this work, you most likely need some way to know in advance what data your React components will need to use, load it separately from those components, and have some way of transferring information about the loaded data from server to client. If you try to implement this in a generalized way, you'll end up reinventing something like the Flux/Redux pattern. 323 | 324 | To avoid inventing your own incomplete version of Flux/Redux, you probably should just use [Redux](https://github.com/reactjs/redux). This is at first a very unfamiliar and tricky-looking abstraction, but does solve all the problems around server-side execution of React apps. To get a working starting point for an ASP.NET Core site with React+Redux on the client (and server-side prerendering), see the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/). 325 | 326 | ## Webpack dev middleware 327 | 328 | If you're using webpack, the webpack dev middleware feature included in `Microsoft.AspNetCore.SpaServices` will streamline your development process. It intercepts requests that would match files built by webpack, and dynamically builds those files on demand. They don't need to be written to disk - they are just held in memory and served directly to the browser. 329 | 330 | Benefits: 331 | 332 | * You don't have to run `webpack` manually or set up any file watchers 333 | * The browser is always guaranteed to receive up-to-date built output 334 | * The built artifacts are normally served instantly or at least extremely quickly, because internally, an instance of `webpack` stays active and has partial compilation states pre-cached in memory 335 | 336 | It lets you work as if the browser natively understands whatever file types you are working with (e.g., TypeScript, SASS), because it's as if there's no build process to wait for. 337 | 338 | ### Example: A simple Webpack setup that builds TypeScript 339 | 340 | **Note:** If you already have Webpack in your project, then you can skip this section. 341 | 342 | As a simple example, here's how you can set up Webpack to build TypeScript files. First install the relevant NPM packages by executing this from the root directory of your project: 343 | 344 | ``` 345 | npm install --save typescript ts-loader 346 | ``` 347 | 348 | And if you don't already have it, you'll find it useful to install the `webpack` command-line tool: 349 | 350 | ``` 351 | npm install -g webpack 352 | ``` 353 | 354 | Now add a Webpack configuration file. Create `webpack.config.js` in the root of your project, containing the following: 355 | 356 | ```javascript 357 | module.exports = { 358 | resolve: { 359 | // For modules referenced with no filename extension, Webpack will consider these extensions 360 | extensions: [ '', '.js', '.ts' ] 361 | }, 362 | module: { 363 | loaders: [ 364 | // This example only configures Webpack to load .ts files. You can also drop in loaders 365 | // for other file types, e.g., .coffee, .sass, .jsx, ... 366 | { test: /\.ts$/, loader: 'ts-loader' } 367 | ] 368 | }, 369 | entry: { 370 | // The loader will follow all chains of reference from this entry point... 371 | main: ['./ClientApp/MyApp.ts'] 372 | }, 373 | output: { 374 | // ... and emit the built result in this location 375 | path: __dirname + '/wwwroot/dist', 376 | filename: '[name].js' 377 | }, 378 | }; 379 | ``` 380 | 381 | Now you can put some TypeScript code (minimally, just `console.log('Hello');`) at `ClientApp/MyApp.ts` and then run `webpack` from the command line to build it (and everything it references). The output will be placed in `wwwroot/dist`, so you can load and run it in a browser by adding the following to one of your views (e.g., `Views\Home\Index.cshtml`): 382 | 383 | 384 | 385 | The Webpack loader, `ts-loader`, follows all chains of reference from `MyApp.ts` and will compile all referenced TypeScript code into your output. If you want, you can create a [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) to control things like whether source maps will be included in the output. If you add other Webpack loaders to your `webpack.config.js`, you can even reference things like SASS from your TypeScript, and then it will get built to CSS and loaded automatically. 386 | 387 | So that's enough to build TypeScript. Here's where webpack dev middleware comes in to auto-build your code whenever needed (so you don't need any file watchers or to run `webpack` manually), and optionally hot module replacement (HMR) to push your changes automatically from code editor to browser without even reloading the page. 388 | 389 | ### Example: A simple Webpack setup that builds LESS 390 | 391 | Following on from the preceding example that builds TypeScript, you could extend your Webpack configuration further to support building LESS. There are three major approaches to doing this: 392 | 393 | 1. **If using Angular, use its native style loader to attach the styles to components**. This is extremely simple and is usually the right choice if you are using Angular. However it only applies to Angular components, not to any other part of the host page, so sometimes you might want to combine this technique with options 2 or 3 below. 394 | 395 | 2. **Or, use Webpack's style loader to attach the styles at runtime**. The CSS markup will be included in your JavaScript bundles and will be attached to the document dynamically. This has certain benefits during development but isn't recommended in production. 396 | 397 | 3. **Or, have each build write a standalone `.css` file to disk**. At runtime, load it using a regular `` tag. This is likely to be the approach you'll want for production use (at least for non-Angular applications, such as React applications) as it's the most robust and best-performing option. 398 | 399 | If instead of LESS you prefer SASS or another CSS preprocessor, the exact same techniques should work, but of course you'll need to replace the `less-loader` with an equivalent Webpack loader for SASS or your chosen preprocessor. 400 | 401 | #### Approach 1: Scoping styles to Angular components 402 | 403 | If you are using Angular, this is the easiest way to perform styling. It works with both server and client rendering, supports Hot Module Replacement, and robustly scopes styles to particular components (and optionally, their descendant elements). 404 | 405 | This repository's Angular template uses this technique to scope styles to components out of the box. It defines those styles as `.css` files. For example, its components reference `.css` files like this: 406 | 407 | ```javascript 408 | @Component({ 409 | ... 410 | styles: [require('./somecomponent.css')] 411 | }) 412 | export class SomeComponent { ... } 413 | ``` 414 | 415 | To make this work, the template has Webpack configured to inject the contents of the `.css` file as a string literal in the built file. Here's the configuration that enables this: 416 | 417 | ```javascript 418 | // This goes into webpack.config.js, in the module loaders array: 419 | { test: /\.css/, include: /ClientApp/, loader: 'raw-loader' } 420 | ``` 421 | 422 | Now if you want to use LESS instead of plain CSS, you just need to include a LESS loader. Run the following in a command prompt at your project root: 423 | 424 | ``` 425 | npm install --save less-loader less 426 | ``` 427 | 428 | Next, add the following loader configuration to the `loaders` array in `webpack.config.js`: 429 | 430 | ```javascript 431 | { test: /\.less/, include: /ClientApp/, loader: 'raw-loader!less-loader' } 432 | ``` 433 | 434 | Notice how this chains together with `less-loader` (which transforms `.less` syntax to plain CSS syntax), then the `raw` loader (which turn the result into a string literal). With this in place, you can reference `.less` files from your Angular components in the obvious way: 435 | 436 | ```javascript 437 | @Component({ 438 | ... 439 | styles: [require('./somecomponent.less')] 440 | }) 441 | export class SomeComponent { ... } 442 | ``` 443 | 444 | ... and your styles will be applied in both server-side and client-side rendering. 445 | 446 | #### Approach 2: Loading the styles using Webpack and JavaScript 447 | 448 | This technique works with any client-side framework (not just Angular), and can also apply styles to the entire document rather than just individual components. It's a little simpler to set up than technique 3, plus it works flawlessly with Hot Module Replacement (HMR). The downside is that it's really only good for development time, because in production you probably don't want users to wait until JavaScript is loaded before styles are applied to the page (this would mean they'd see a 'flash of unstyled content' while the page is being loaded). 449 | 450 | First create a `.less` file in your project. For example, create a file at `ClientApp/styles/mystyles.less` containing: 451 | 452 | ```less 453 | @base: #f938ab; 454 | 455 | h1 { 456 | color: @base; 457 | } 458 | ``` 459 | 460 | Reference this file from an `import` or `require` statement in one of your JavaScript or TypeScript files. For example, if you've got a `boot-client.ts` file, add the following near the top: 461 | 462 | ```javascript 463 | import './styles/mystyles.less'; 464 | ``` 465 | 466 | If you try to run the Webpack compiler now (e.g., via `webpack` on the command line), you'll get an error saying it doesn't know how to build `.less` files. So, it's time to install a Webpack loader for LESS (plus related NPM modules). In a command prompt at your project's root directory, run: 467 | 468 | ``` 469 | npm install --save less-loader less 470 | ``` 471 | 472 | Finally, tell Webpack to use this whenever it encounters a `.less` file. In `webpack.config.js`, add to the `loaders` array: 473 | 474 | ``` 475 | { test: /\.less/, loader: 'style-loader!css-loader!less-loader' } 476 | ``` 477 | 478 | This means that when you `import` or `require` a `.less` file, it should pass it first to the LESS compiler to produce CSS, then the output goes to the CSS and Style loaders that know how to attach it dynamically to the page at runtime. 479 | 480 | That's all you need to do! Restart your site and you should see the LESS styles being applied. This technique is compatible with both source maps and Hot Module Replacement (HMR), so you can edit your `.less` files at will and see the changes appearing live in the browser. 481 | 482 | #### Approach 3: Building LESS to CSS files on disk 483 | 484 | This technique takes a little more work to set up than technique 2, and lacks compatibility with HMR. But it's much better for production use if your styles are applied to the whole page (not just elements constructed via JavaScript), because it loads the CSS independently of JavaScript. 485 | 486 | First add a `.less` file into your project. For example, create a file at `ClientApp/styles/mystyles.less` containing: 487 | 488 | ```less 489 | @base: #f938ab; 490 | 491 | h1 { 492 | color: @base; 493 | } 494 | ``` 495 | 496 | Reference this file from an `import` or `require` statement in one of your JavaScript or TypeScript files. For example, if you've got a `boot-client.ts` file, add the following near the top: 497 | 498 | ```javascript 499 | import './styles/mystyles.less'; 500 | ``` 501 | 502 | If you try to run the Webpack compiler now (e.g., via `webpack` on the command line), you'll get an error saying it doesn't know how to build `.less` files. So, it's time to install a Webpack loader for LESS (plus related NPM modules). In a command prompt at your project's root directory, run: 503 | 504 | ``` 505 | npm install --save less less-loader extract-text-webpack-plugin 506 | ``` 507 | 508 | Next, you can extend your Webpack configuration to handle `.less` files. In `webpack.config.js`, at the top, add: 509 | 510 | ```javascript 511 | var extractStyles = new (require('extract-text-webpack-plugin'))('mystyles.css'); 512 | ``` 513 | 514 | This creates a plugin instance that will output text to a file called `mystyles.css`. You can now compile `.less` files and emit the resulting CSS text into that file. To do so, add the following to the `loaders` array in your Webpack configuration: 515 | 516 | ```javascript 517 | { test: /\.less$/, loader: extractStyles.extract('css-loader!less-loader') } 518 | ``` 519 | 520 | This tells Webpack that, whenever it finds a `.less` file, it should use the LESS loader to produce CSS, and then feed that CSS into the `extractStyles` object which you've already configured to write a file on disk called `mystyles.css`. Finally, for this to actually work, you need to include `extractStyles` in the list of active plugins. Just add that object to the `plugins` array in your Webpack config, e.g.: 521 | 522 | ```javascript 523 | plugins: [ 524 | extractStyles, 525 | ... leave any other plugins here ... 526 | ] 527 | ``` 528 | 529 | If you run `webpack` on the command line now, you should now find that it emits a new file at `dist/mystyles.css`. You can make browsers load this file simply by adding a regular `` tag. For example, in `Views/Shared/_Layout.cshtml`, add: 530 | 531 | ```html 532 | 533 | ``` 534 | 535 | **Note:** This technique (writing the built `.css` file to disk) is ideal for production use. But note that, at development time, *it does not support Hot Module Replacement (HMR)*. You will need to reload the page each time you edit your `.less` file. This is a known limitation of `extract-text-webpack-plugin`. If you have constructive opinions on how this can be improved, see the [discussion here](https://github.com/webpack/extract-text-webpack-plugin/issues/30). 536 | 537 | ### Enabling webpack dev middleware 538 | 539 | First install the `Microsoft.AspNetCore.SpaServices` NuGet package and the `aspnet-webpack` NPM package, then go to your `Startup.cs` file, and **before your call to `UseStaticFiles`**, add the following: 540 | 541 | ```csharp 542 | if (env.IsDevelopment()) { 543 | app.UseWebpackDevMiddleware(); 544 | } 545 | 546 | // Your call to app.UseStaticFiles(); should be here 547 | ``` 548 | 549 | Also check your webpack configuration at `webpack.config.js`. Since `UseWebpackDevMiddleware` needs to know which incoming requests to intercept, make sure you've specified a `publicPath` value on your `output`, for example: 550 | 551 | ```javascript 552 | module.exports = { 553 | // ... rest of your webpack config is here ... 554 | 555 | output: { 556 | path: path.join(__dirname, 'wwwroot', 'dist'), 557 | publicPath: '/dist/', 558 | filename: '[name].js' 559 | }, 560 | }; 561 | ``` 562 | 563 | Now, assuming you're running in [development mode](https://docs.asp.net/en/latest/fundamentals/environments.html), any requests for files under `/dist` will be intercepted and served using Webpack dev middleware. 564 | 565 | **This is for development time only, not for production use (hence the `env.IsDevelopment()` check in the code above).** While you could technically remove that check and serve your content in production through the webpack middleware, it's hard to think of a good reason for doing so. For best performance, it makes sense to prebuild your client-side resources so they can be served directly from disk with no build middleware. If you use the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/), you'll get a site that produces optimised static builds for production, while also supporting webpack dev middleware at development time. 566 | 567 | ## Webpack Hot Module Replacement 568 | 569 | For an even more streamlined development experience, you can enhance webpack dev middleware by enabling Hot Module Replacement (HMR) support. This watches for any changes you make to source files on disk (e.g., `.ts`/`.html`/`.sass`/etc. files), and automatically rebuilds them and pushes the result into your browser window, without even needing to reload the page. 570 | 571 | This is *not* the same as a simple live-reload mechanism. It does not reload the page; it replaces code or markup directly in place. This is better, because it does not interfere with any state your SPA might have in memory, or any debugging session you have in progress. 572 | 573 | Typically, when you change a source file, the effects appear in your local browser window in under 2 seconds, even when your overall application is large. This is superbly productive, especially in multi-monitor setups. If you cause a build error (e.g., a syntax error), details of the error will appear in your browser window. When you fix it, your application will reappear, without having lost its in-memory state. 574 | 575 | ### Enabling Hot Module Replacement 576 | 577 | First ensure you already have a working Webpack dev middleware setup. Then, install the `webpack-hot-middleware` NPM module: 578 | 579 | ``` 580 | npm install --save-dev webpack-hot-middleware 581 | ``` 582 | 583 | At the top of your `Startup.cs` file, add the following namespace reference: 584 | 585 | ```csharp 586 | using Microsoft.AspNetCore.SpaServices.Webpack; 587 | ``` 588 | 589 | Now amend your call to `UseWebpackDevMiddleware` as follows: 590 | 591 | ```csharp 592 | app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { 593 | HotModuleReplacement = true 594 | }); 595 | ``` 596 | 597 | Also, to work around a temporary issue in `SpaServices`, you must ensure that your Webpack config includes a `plugins` array, even if it's empty. For example, in `webpack.config.js`: 598 | 599 | ```javascript 600 | module.exports = { 601 | // ... rest of your webpack config is here ... 602 | 603 | plugins: [ 604 | // Put webpack plugins here if needed, or leave it as an empty array if not 605 | ] 606 | }; 607 | ``` 608 | 609 | Now when you load your application in a browser, you should see a message like the following in your browser console: 610 | 611 | ``` 612 | [HMR] connected 613 | ``` 614 | 615 | If you edit any of your source files that get built by webpack, the result will automatically be pushed into the browser. As for what the browser does with these updates - that's a matter of how you configure it - see below. 616 | 617 | **Note for TypeScript + Visual Studio users** 618 | 619 | If you want HMR to work correctly with TypeScript, and you use Visual Studio on Windows as an IDE (but not VS Code), then you will need to make a further configuration change. In your `.csproj` file, in one of the `` elements, add this: 620 | 621 | true 622 | 623 | This is necessary because otherwise, Visual Studio will try to auto-compile TypeScript files as you save changes to them. That default auto-compilation behavior is unhelpful in projects where you have a proper build system (e.g., Webpack), because VS doesn't know about your build system and would emit `.js` files in the wrong locations, which would in turn cause problems with your real build or deployment mechanisms. 624 | 625 | #### Enabling hot replacement for React components 626 | 627 | Webpack has built-in support for updating React components in place. To enable this, amend your `UseWebpackDevMiddleware` call further as follows: 628 | 629 | ```csharp 630 | app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { 631 | HotModuleReplacement = true, 632 | ReactHotModuleReplacement = true 633 | }); 634 | ``` 635 | 636 | Also, install the NPM module `aspnet-webpack-react`, e.g.: 637 | 638 | ``` 639 | npm install --save-dev aspnet-webpack-react 640 | ``` 641 | 642 | Now if you edit any React component (e.g., in `.jsx` or `.tsx` files), the updated component will be injected into the running application, and will even preserve its in-memory state. 643 | 644 | **Note**: In you webpack config, be sure that your React components are loaded using `babel-loader` (and *not* just directly using `babel` or `ts-loader`), because `babel-loader` is where the HMR instrumentation is injected. For an example of HMR for React components built with TypeScript, see the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/). 645 | 646 | #### Enabling hot replacement for other module types 647 | 648 | Webpack has built-in HMR support for various types of module, such as styles and React components as described above. But to support HMR for other code modules, you need to add a small block of code that calls `module.hot.accept` to receive the updated module and update the running application. 649 | 650 | This is [documented in detail on the Webpack site](https://webpack.github.io/docs/hot-module-replacement.html). Or to get a working HMR-enabled ASP.NET Core site with Angular, React, React+Redux, or Knockout, you can use the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/). 651 | 652 | #### Passing options to the Webpack Hot Middleware client 653 | 654 | You can configure the [Webpack Hot Middleware client](https://github.com/glenjamin/webpack-hot-middleware#client) 655 | by using the `HotModuleReplacementClientOptions` property on `WebpackDevMiddlewareOptions`: 656 | 657 | ```csharp 658 | app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { 659 | HotModuleReplacement = true, 660 | HotModuleReplacementClientOptions = new Dictionary { 661 | { "reload", "true" }, 662 | }, 663 | }); 664 | ``` 665 | 666 | For the list of available options, please see [Webpack Hot Middleware docs](https://github.com/glenjamin/webpack-hot-middleware#client). 667 | 668 | **Note**: The `path` option cannot be overridden this way - it is controlled by the `HotModuleReplacementEndpoint` setting. 669 | 670 | ## Routing helper: MapSpaFallbackRoute 671 | 672 | In most single-page applications, you'll want client-side routing as well as your server-side routing. Most of the time, the two routing systems work independently without interfering. However, there is one case where things get challenging: identifying 404s. 673 | 674 | If a request arrives for `/some/page`, and it doesn't match any server-side route, it's likely that you want to return HTML that starts up your client-side application, which probably understands the route `/some/page`. But if a request arrives for `/images/user-512.png`, and it doesn't match any server-side route or static file, it's **not** likely that your client-side application would handle it - you probably want to return a 404. 675 | 676 | To help distinguish between these cases, the `Microsoft.AspNetCore.SpaServices` NuGet package includes a routing helper, `MapSpaFallbackRoute`. For example, in your `Startup.cs` file's `Configure` method, you might add: 677 | 678 | ```csharp 679 | app.UseStaticFiles(); 680 | 681 | app.UseMvc(routes => 682 | { 683 | routes.MapRoute( 684 | name: "default", 685 | template: "{controller=Home}/{action=Index}/{id?}"); 686 | 687 | routes.MapSpaFallbackRoute( 688 | name: "spa-fallback", 689 | defaults: new { controller = "Home", action = "Index" }); 690 | }); 691 | ``` 692 | 693 | Since `UseStaticFiles` goes first, any requests that actually match physical files under `wwwroot` will be handled by serving that static file. 694 | 695 | Since the default server-side MVC route goes next, any requests that match existing controller/action pairs will be handled by invoking that action. 696 | 697 | Then, since `MapSpaFallbackRoute` is last, any other requests **that don't appear to be for static files** will be served by invoking the `Index` action on `HomeController`. This action's view should serve your client-side application code, allowing the client-side routing system to handle whatever URL has been requested. 698 | 699 | Any requests that do appear to be for static files (i.e., those that end with filename extensions), will *not* be handled by `MapSpaFallbackRoute`, and so will end up as 404s. 700 | 701 | This is not a perfect solution to the problem of identifying 404s, because for example `MapSpaFallbackRoute` will not match requests for `/users/albert.einstein`, because it appears to contain a filename extension (`.einstein`). If you need your SPA to handle routes like that, then don't use `MapSpaFallbackRoute` - just use a regular MVC catch-all route. But then beware that requests for unknown static files will result in your client-side app being rendered. 702 | 703 | ## Debugging your projects 704 | 705 | How to attach and use a debugger depends on what code you want to debug. For details, see: 706 | 707 | * [How to debug your C# code that runs on the server](#debugging-your-c-code-that-runs-on-the-server) 708 | * How to debug your JavaScript/TypeScript code: 709 | * ... [when it's running in a browser](#debugging-your-javascripttypescript-code-when-its-running-in-a-browser) 710 | * ... [when it's running on the server](#debugging-your-javascripttypescript-code-when-it-runs-on-the-server) (i.e., via `asp-prerender` or NodeSevices) 711 | 712 | ### Debugging your C# code that runs on the server 713 | 714 | You can use any .NET debugger, for example Visual Studio's C# debugger or [Visual Studio Code's C# debugger](https://code.visualstudio.com/Docs/editor/debugging). 715 | 716 | ### Debugging your JavaScript/TypeScript code when it's running in a browser 717 | 718 | **The absolute most reliable way of debugging your client-side code is to use your browser's built-in debugger.** This is much easier to make work than debugging via an IDE, plus it offers much richer insight into what's going on than your IDE will do (for example, you'll be able to inspect the DOM and capture performance profiles as well as just set breakpoints and step through code). 719 | 720 | If you're unfamiliar with your browser's debugging tools, then take the time to get familiar with them. You will become more productive. 721 | 722 | #### Using your browser's built-in debugging tools 723 | 724 | ##### Using Chrome's developer tools for debugging 725 | 726 | In Chrome, with your application running in the browser, [open the developer tools](https://developer.chrome.com/devtools#access). You can now find your code: 727 | 728 | * In the developer tools *Sources* tab, expand folders in the hierarchy pane on the left to find the file you want 729 | * Or, press `ctrl`+`o` (on Windows) or `cmd`+`o` on Mac, then start to type name name of the file you want to open (e.g., `counter.component.ts`) 730 | 731 | With source maps enabled (which is the case in the project templates in this repo), you'll be able to see your original TypeScript source code, set breakpoints on it, etc. 732 | 733 | ##### Using Internet Explorer/Edge's developer tools (F12) for debugging 734 | 735 | In Internet Explorer or Edge, with your application running in the browser, open the F12 developer tools by pressing `F12`. You can now find your code: 736 | 737 | * In the F12 tools *Debugger* tab, expand folders in the hierarchy pane on the left to find the file you want 738 | * Or, press `ctrl`+`o`, then start to type name name of the file you want to open (e.g., `counter.component.ts`) 739 | 740 | With source maps enabled (which is the case in the project templates in this repo), you'll be able to see your original TypeScript source code, set breakpoints on it, etc. 741 | 742 | ##### Using Firefox's developer tools for debugging 743 | 744 | In Firefox, with your application running in the browser, open the developer tools by pressing `F12`. You can now find your code: 745 | 746 | * In the developer tools *Debugger* tab, expand folders in the hierarchy pane titled *Sources* towards the bottom to find the file you want 747 | * Or, press `ctrl`+`o` (on Windows) or `cmd`+`o` on Mac, then start to type name name of the file you want to open (e.g., `counter.component.ts`) 748 | 749 | With source maps enabled (which is the case in the project templates in this repo), you'll be able to see your original TypeScript source code, set breakpoints on it, etc. 750 | 751 | ##### How browser-based debugging interacts with Hot Module Replacement (HMR) 752 | 753 | If you're using HMR, then each time you modify a file, the Webpack dev middleware restarts your client-side application, adding a new version of each affected module, without reloading the page. This can be confusing during debugging, because any breakpoints set on the old version of the code will still be there, but they will no longer get hit, because the old version of the module is no longer in use. 754 | 755 | You have two options to get breakpoints that will be hit as expected: 756 | 757 | * **Reload the page** (e.g., by pressing `F5`). Then your existing breakpoints will be applied to the new version of the module. This is obviously the easiest solution. 758 | * Or, if you don't want to reload the page, you can **set new breakpoints on the new version of the module**. To do this, look in your browser's debug tools' list of source files, and identify the newly-injected copy of the module you want to debug. It will typically have a suffix on its URL such as `?4a2c`, and may appear in a new top-level hierarchy entry called `webpack://`. Set a breakpoint in the newly-injected module, and it will be hit as expected as your application runs. 759 | 760 | #### Using Visual Studio Code's "Debugger for Chrome" extension 761 | 762 | If you're using Visual Studio Code and Chrome, you can set breakpoints directly on your TypeScript source code in the IDE. To do this: 763 | 764 | 1. Install VS Code's [*Debugger for Chrome* extension](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) 765 | 2. Ensure your application server has started and can be reached with a browser (for example, run `dotnet watch run`) 766 | 3. In VS Code, open its *Debug* view (on Windows/Linux, press `ctrl`+`shift`+`d`; on Mac, press `cmd`+`shift`+`d`). 767 | 4. Press the cog icon and when prompted to *Select environment*, choose `Chrome`. VS Code will create a `launch.json` file for you. This describes how the debugger and browser should be launched. 768 | 5. Edit your new `.vscode/launch.json` file to specify the correct `url` and `webRoot` for your application. If you're using the project templates in this repo, then the values you probably want are: 769 | * For `url`, put `"http://localhost:5000"` (but of course, change this if you're using a different port) 770 | * For `port`, put `5000` (or your custom port number if applicable) 771 | * For `workspace` in **both** configurations, put `"${workspaceRoot}/wwwroot"` 772 | * This tells the debugger how URLs within your application correspond to files in your VS Code workspace. By default, ASP.NET Core projects treat `wwwroot` as the root directory for publicly-served files, so `http://localhost:5000/dist/myfile.js` corresponds to `/wwwroot/dist/myfile.js`. VS Code doesn't know about `wwwroot` unless you tell it. 773 | * **Important:** If your VS Code window's workspace root is not the same as your ASP.NET Core project root (for example, if VS Code is opened at a higher-level directory to show both your ASP.NET Core project plus other peer-level directories), then you will need to amend `workspace` correspondingly (e.g., to `"${workspaceRoot}/SomeDir/MyAspNetProject/wwwroot"`). 774 | 6. Start the debugger: 775 | * While still on the *Debug* view, from the dropdown near the top-left, choose "*Launch Chrome against localhost, with sourcemaps*". 776 | * Press the *Play* icon. Your application will launch in Chrome. 777 | * If it does nothing for a while, then eventually gives the error *Cannot connect to runtime process*, that's because you already have an instance of Chrome running. Close it first, then try again. 778 | 7. Finally, you can now set and hit breakpoints in your TypeScript code in VS Code. 779 | 780 | For more information about VS Code's built-in debugging facilities, [see its documentation](https://code.visualstudio.com/Docs/editor/debugging). 781 | 782 | Caveats: 783 | 784 | * The debugging interface between VS Code and Chrome occasionally has issues. If you're unable to set or hit breakpoints, or if you try to set a breakpoint but it appears in the wrong place, you may need to stop and restart the debugger (and often, the whole Chrome process). 785 | * If you're using Hot Module Replacement (HMR), then whenever you edit a file, the breakpoints in it will no longer hit. This is because HMR loads a new version of the module into the browser, so the old code no longer runs. To fix this, you must: 786 | * Reload the page in Chrome (e.g., by pressing `F5`) 787 | * **Then** (and only then), remove and re-add the breakpoint in VS Code. It will now be attached to the current version of your module. Alternatively, stop and restart debugging altogether. 788 | * If you prefer, you can use "*Attach to Chrome, with sourcemaps*" instead of launching a new Chrome instance, but this is a bit trickier: you must first start Chrome using the command-line option `--remote-debugging-port=9222`, and you must ensure there are no other tabs opened (otherwise, it might try to connect to the wrong one). 789 | 790 | 791 | #### Using Visual Studio's built-in debugger for Internet Explorer 792 | 793 | If you're using Visual Studio on Windows, and are running your app in Internet Explorer 11 (not Edge!), then you can use VS's built-in debugger rather than Interner Explorer's F12 tools if you prefer. To do this: 794 | 795 | 1. In Internet Explorer, [enable script debugging](https://msdn.microsoft.com/en-us/library/ms241741\(v=vs.100\).aspx) 796 | 2. In Visual Studio, [set the default "*Browse with*" option](http://stackoverflow.com/a/31959053) to Internet Explorer 797 | 3. In Visual Studio, press F5 to launch your application with the debugger in Internet Explorer. 798 | * When the page has loaded in the browser, you'll be able to set and hit breakpoints in your TypeScript source files in Visual Studio. 799 | 800 | Caveats: 801 | 802 | * If you're using Hot Module Replacement, you'll need to stop and restart the debugger any time you change a source file. VS's IE debugger does not recognise that source files might change while the debugging session is in progress. 803 | * Realistically, you are not going to be as productive using this approach to debugging as you would be if you used your browser's built-in debugging tools. The browser's built-in debugging tools are far more effective: they are always available (you don't have to have launched your application in a special way), they better handle HMR, and they don't make your application very slow to launch. 804 | 805 | ## Debugging your JavaScript/TypeScript code when it runs on the server 806 | 807 | When you're using NodeServices or the server-side prerendering feature included in the project templates in this repo, your JavaScript/TypeScript code will execute on the server in a background instance of Node.js. You can enable debugging via [V8 Inspector Integration](https://nodejs.org/api/debugger.html#debugger_v8_inspector_integration_for_node_js) on that Node.js instance. Here's how to do it. 808 | 809 | First, in your `Startup.cs` file, in the `ConfigureServices` method, add the following: 810 | 811 | ``` 812 | services.AddNodeServices(options => { 813 | options.LaunchWithDebugging = true; 814 | options.DebuggingPort = 9229; 815 | }); 816 | ``` 817 | 818 | Now, run your application from that command line (e.g., `dotnet run`). Then in a browser visit one of your pages that causes server-side JS to execute. 819 | 820 | In the console, you should see all the normal trace messages appear, plus among them will be: 821 | 822 | ``` 823 | warn: Microsoft.AspNetCore.NodeServices[0] 824 | Debugger listening on port 9229. 825 | warn: Microsoft.AspNetCore.NodeServices[0] 826 | Warning: This is an experimental feature and could change at any time. 827 | warn: Microsoft.AspNetCore.NodeServices[0] 828 | To start debugging, open the following URL in Chrome: 829 | warn: Microsoft.AspNetCore.NodeServices[0] 830 | chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 831 | ``` 832 | 833 | As per instructions open the URL in Chrome. Alternatively you can go to the `Sources` tab of the Dev Tools (at http://localhost:5000) and connect to the Node instance under `Threads` in the right sidebar. 834 | 835 | By expanding the `webpack://` entry in the sidebar, you'll be able to find your original source code (it's using source maps), and then set breakpoints in it. When you re-run your app in another browser window, your breakpoints will be hit, then you can debug the server-side execution just like you'd debug client-side execution. It looks like this: 836 | 837 | ![screenshot from 2017-03-25 13-33-26](https://cloud.githubusercontent.com/assets/1596280/24324604/ab888a7e-115f-11e7-89d1-1586acf5e35c.png) 838 | 839 | --------------------------------------------------------------------------------