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.
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 | [](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 | 
838 |
839 |
--------------------------------------------------------------------------------