Skip to content

Commit 1f04c03

Browse files
Add support for webpack app manifest (reactjs#1028)
* Add support for webpack app manifest * Implement automatic manifest reloading * Flag asset manifest feature as a beta
1 parent fa52829 commit 1f04c03

File tree

18 files changed

+495
-52
lines changed

18 files changed

+495
-52
lines changed

site/jekyll/bundling/cassette.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Cassette (ASP.NET 4.x)
55

66
> **Note:**
77
>
8-
> This guide applies only to ASP.NET 4.x
8+
> This guide applies only to ASP.NET 4.x. Please consider using [webpack](/bundling/webpack.html) if possible.
99
1010
Just want to see the code? Check out the [sample project](https://github.com/reactjs/React.NET/tree/master/src/React.Sample.Cassette).
1111

site/jekyll/bundling/msbuild.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: MSBuild (ASP.NET 4.x)
55

66
> **Note:**
77
>
8-
> This guide applies only to ASP.NET 4.x
8+
> This guide applies only to ASP.NET 4.x. Please consider using [webpack](/bundling/webpack.html) if possible.
99
1010
Just want to see the code? Check out the [sample project](https://github.com/reactjs/React.NET/tree/master/src/React.Sample.Mvc4).
1111

site/jekyll/bundling/weboptimizer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Bundling and Minification (ASP.NET 4.x)
55

66
> **Note:**
77
>
8-
> This guide applies only to ASP.NET 4.x
8+
> This guide applies only to ASP.NET 4.x. Please consider using [webpack](/bundling/webpack.html) if possible.
99
1010
Just want to see the code? Check out the [sample project](https://github.com/reactjs/React.NET/tree/master/src/React.Sample.Mvc4).
1111

site/jekyll/bundling/webpack.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ global.Components = { RootComponent };
4545

4646
Once Webpack has been configured, run `npm run build` to build the bundles. Once you have verified that the bundle is being created correctly, you can modify your ReactJS.NET configuration (normally `App_Start\ReactConfig.cs`) to load the newly-created bundle.
4747

48+
Reference the runtime, vendor, and main app bundles that were generated:
49+
4850
```csharp
4951
ReactSiteConfiguration.Configuration
5052
.SetLoadBabel(false)
5153
.SetLoadReact(false)
5254
.AddScriptWithoutTransform("~/dist/runtime.js")
5355
.AddScriptWithoutTransform("~/dist/vendor.js")
54-
.AddScriptWithoutTransform("~/dist/components.js");
56+
.AddScriptWithoutTransform("~/dist/main.js");
5557
```
5658

5759
This will load all your components into the `Components` global, which can be used from `Html.React` to render any of the components:
@@ -71,11 +73,44 @@ Reference the built bundle directly in a script tag at the end of the page in `_
7173
// at the top of your layout
7274
@using React.AspNet
7375

74-
// before the closing </body> tag
75-
<script src="/dist/runtime.js"></script>
76-
<script src="/dist/vendor.js"></script>
77-
<script src="/dist/components.js"></script>
78-
@Html.ReactInitJavaScript()
76+
<head>
77+
<link rel="stylesheet" href="/dist/main.css">
78+
</head>
79+
<body>
80+
@RenderBody()
81+
<script src="/dist/runtime.js"></script>
82+
<script src="/dist/vendor.js"></script>
83+
<script src="/dist/main.js"></script>
84+
@Html.ReactInitJavascript()
85+
</body>
7986
```
8087

8188
A full example is available in [the ReactJS.NET repository](https://github.com/reactjs/React.NET/tree/master/src/React.Sample.Webpack.CoreMvc).
89+
90+
### 💡 Beta feature: Asset manifest handling
91+
92+
An asset manifest is generated by the `webpack-asset-manifest` plugin, written to `asset-manifest.json`. See the webpack config example above for details on how to set this up. This manifest file contains a list of all of the bundles required to run your app. To use it, call `.SetReactAppBuildPath("~/dist")`. You may still provide exact paths to additional scripts by calling `AddScriptWithoutTransform("~/dist/path-to-your-file.js")`.
93+
94+
```csharp
95+
ReactSiteConfiguration.Configuration
96+
.SetLoadBabel(false)
97+
.SetLoadReact(false)
98+
.SetReactAppBuildPath("~/dist");
99+
```
100+
101+
Then, make calls to `@Html.ReactGetScriptPaths()` and `@Html.ReactGetStylePaths()` where you would normally reference styles and scripts from your layout.
102+
103+
```html
104+
// at the top of your layout
105+
@using React.AspNet
106+
107+
<head>
108+
@Html.ReactGetStylePaths()
109+
</head>
110+
<body>
111+
@RenderBody()
112+
113+
@Html.ReactGetScriptPaths()
114+
@Html.ReactInitJavascript()
115+
</body>
116+
```

src/React.AspNet/HtmlHelperExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using System.IO;
10+
using System.Linq;
1011
using System.Text;
1112

1213
#if LEGACYASPNET
@@ -169,6 +170,32 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
169170
}
170171
}
171172

173+
/// <summary>
174+
/// Returns script tags based on the webpack asset manifest
175+
/// </summary>
176+
/// <param name="htmlHelper"></param>
177+
/// <returns></returns>
178+
public static IHtmlString ReactGetScriptPaths(this IHtmlHelper htmlHelper)
179+
{
180+
string nonce = Environment.Configuration.ScriptNonceProvider != null
181+
? $" nonce=\"{Environment.Configuration.ScriptNonceProvider()}\""
182+
: "";
183+
184+
return new HtmlString(string.Join("", Environment.GetScriptPaths()
185+
.Select(scriptPath => $"<script{nonce} src=\"{scriptPath}\"></script>")));
186+
}
187+
188+
/// <summary>
189+
/// Returns style tags based on the webpack asset manifest
190+
/// </summary>
191+
/// <param name="htmlHelper"></param>
192+
/// <returns></returns>
193+
public static IHtmlString ReactGetStylePaths(this IHtmlHelper htmlHelper)
194+
{
195+
return new HtmlString(string.Join("", Environment.GetStylePaths()
196+
.Select(stylePath => $"<link rel=\"stylesheet\" href=\"{stylePath}\" />")));
197+
}
198+
172199
private static IHtmlString RenderToString(Action<StringWriter> withWriter)
173200
{
174201
var stringWriter = _sharedStringWriter;

src/React.Core/IReactEnvironment.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
*/
77

88

9+
using System.Collections.Generic;
910
using System.IO;
1011

1112
namespace React
1213
{
1314
/// <summary>
14-
/// Request-specific ReactJS.NET environment. This is unique to the individual request and is
15+
/// Request-specific ReactJS.NET environment. This is unique to the individual request and is
1516
/// not shared.
1617
/// </summary>
1718
public interface IReactEnvironment
@@ -51,12 +52,12 @@ public interface IReactEnvironment
5152

5253
/// <summary>
5354
/// Attempts to execute the provided JavaScript code using a non-pooled JavaScript engine (ie.
54-
/// creates a new JS engine per-thread). This is because Babel uses a LOT of memory, so we
55+
/// creates a new JS engine per-thread). This is because Babel uses a LOT of memory, so we
5556
/// should completely dispose any engines that have loaded Babel in order to conserve memory.
56-
///
57+
///
5758
/// If an exception is thrown, retries the execution using a new thread (and hence a new engine)
5859
/// with a larger maximum stack size.
59-
/// This is required because JSXTransformer uses a huge stack which ends up being larger
60+
/// This is required because JSXTransformer uses a huge stack which ends up being larger
6061
/// than what ASP.NET allows by default (256 KB).
6162
/// </summary>
6263
/// <typeparam name="T">Type to return from JavaScript call</typeparam>
@@ -93,7 +94,7 @@ public interface IReactEnvironment
9394
IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false);
9495

9596
/// <summary>
96-
/// Renders the JavaScript required to initialise all components client-side. This will
97+
/// Renders the JavaScript required to initialise all components client-side. This will
9798
/// attach event handlers to the server-rendered HTML.
9899
/// </summary>
99100
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
@@ -116,12 +117,22 @@ public interface IReactEnvironment
116117
IReactSiteConfiguration Configuration { get; }
117118

118119
/// <summary>
119-
/// Renders the JavaScript required to initialise all components client-side. This will
120+
/// Renders the JavaScript required to initialise all components client-side. This will
120121
/// attach event handlers to the server-rendered HTML.
121122
/// </summary>
122123
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
123124
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
124125
/// <returns>JavaScript for all components</returns>
125126
void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
127+
128+
/// <summary>
129+
/// Returns a list of paths to scripts generated by the React app
130+
/// </summary>
131+
IEnumerable<string> GetScriptPaths();
132+
133+
/// <summary>
134+
/// Returns a list of paths to stylesheets generated by the React app
135+
/// </summary>
136+
IEnumerable<string> GetStylePaths();
126137
}
127138
}

src/React.Core/IReactSiteConfiguration.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public interface IReactSiteConfiguration
1919
/// <summary>
2020
/// Adds a script to the list of scripts that are executed. This should be called for all
2121
/// React components and their dependencies. If the script does not have any JSX in it
22-
/// (for example, it's built using Webpack or Gulp), use
22+
/// (for example, it's built using Webpack or Gulp), use
2323
/// <see cref="AddScriptWithoutTransform"/> instead.
2424
/// </summary>
2525
/// <param name="filename">
26-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
26+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
2727
/// <c>~/Scripts/Awesome.js</c>)
2828
/// </param>
2929
/// <returns>This configuration, for chaining</returns>
@@ -35,7 +35,7 @@ public interface IReactSiteConfiguration
3535
/// more efficient.
3636
/// </summary>
3737
/// <param name="filename">
38-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
38+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
3939
/// <c>~/Scripts/Awesome.js</c>)
4040
/// </param>
4141
/// <returns>The configuration, for chaining</returns>
@@ -48,15 +48,15 @@ public interface IReactSiteConfiguration
4848
IEnumerable<string> Scripts { get; }
4949

5050
/// <summary>
51-
/// Gets a list of all the scripts that have been added to this configuration and do not
51+
/// Gets a list of all the scripts that have been added to this configuration and do not
5252
/// require JSX transformation to be run.
5353
/// </summary>
54-
IEnumerable<string> ScriptsWithoutTransform { get; }
54+
IEnumerable<string> ScriptsWithoutTransform { get; }
5555

5656
/// <summary>
5757
/// Gets or sets whether JavaScript engines should be reused across requests.
5858
/// </summary>
59-
///
59+
///
6060
bool ReuseJavaScriptEngines { get; set; }
6161
/// <summary>
6262
/// Sets whether JavaScript engines should be reused across requests.
@@ -79,23 +79,23 @@ public interface IReactSiteConfiguration
7979
IReactSiteConfiguration SetJsonSerializerSettings(JsonSerializerSettings settings);
8080

8181
/// <summary>
82-
/// Gets or sets the number of engines to initially start when a pool is created.
82+
/// Gets or sets the number of engines to initially start when a pool is created.
8383
/// Defaults to <c>10</c>.
8484
/// </summary>
8585
int? StartEngines { get; set; }
8686
/// <summary>
87-
/// Sets the number of engines to initially start when a pool is created.
87+
/// Sets the number of engines to initially start when a pool is created.
8888
/// Defaults to <c>10</c>.
8989
/// </summary>
9090
IReactSiteConfiguration SetStartEngines(int? startEngines);
9191

9292
/// <summary>
93-
/// Gets or sets the maximum number of engines that will be created in the pool.
93+
/// Gets or sets the maximum number of engines that will be created in the pool.
9494
/// Defaults to <c>25</c>.
9595
/// </summary>
9696
int? MaxEngines { get; set; }
9797
/// <summary>
98-
/// Sets the maximum number of engines that will be created in the pool.
98+
/// Sets the maximum number of engines that will be created in the pool.
9999
/// Defaults to <c>25</c>.
100100
/// </summary>
101101
IReactSiteConfiguration SetMaxEngines(int? maxEngines);
@@ -129,7 +129,7 @@ public interface IReactSiteConfiguration
129129
/// </summary>
130130
bool LoadReact { get; set; }
131131
/// <summary>
132-
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
132+
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
133133
/// provide your own version of React.
134134
/// </summary>
135135
/// <returns>The configuration, for chaining</returns>
@@ -204,17 +204,29 @@ public interface IReactSiteConfiguration
204204
IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, string> handler);
205205

206206
/// <summary>
207-
/// A provider that returns a nonce to be used on any script tags on the page.
207+
/// A provider that returns a nonce to be used on any script tags on the page.
208208
/// This value must match the nonce used in the Content Security Policy header on the response.
209209
/// </summary>
210210
Func<string> ScriptNonceProvider { get; set; }
211211

212212
/// <summary>
213-
/// Sets a provider that returns a nonce to be used on any script tags on the page.
213+
/// Sets a provider that returns a nonce to be used on any script tags on the page.
214214
/// This value must match the nonce used in the Content Security Policy header on the response.
215215
/// </summary>
216216
/// <param name="provider"></param>
217217
/// <returns></returns>
218218
IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider);
219+
220+
/// <summary>
221+
/// The path to the application bundles built by webpack or create-react-app
222+
/// </summary>
223+
string ReactAppBuildPath { get; set; }
224+
225+
/// <summary>
226+
/// Sets the path to the application bundles built by webpack or create-react-app
227+
/// </summary>
228+
/// <param name="reactAppBuildPath"></param>
229+
/// <returns></returns>
230+
IReactSiteConfiguration SetReactAppBuildPath(string reactAppBuildPath);
219231
}
220232
}

src/React.Core/JavaScriptEngineFactory.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ IFileSystem fileSystem
8484
protected virtual IJsPool CreatePool()
8585
{
8686
var allFiles = _config.Scripts
87-
.Concat(_config.ScriptsWithoutTransform)
88-
.Select(_fileSystem.MapPath);
87+
.Concat(_config.ScriptsWithoutTransform)
88+
.Concat(_config.ReactAppBuildPath != null
89+
? new[] { $"{_config.ReactAppBuildPath}/asset-manifest.json"}
90+
: Enumerable.Empty<string>())
91+
.Select(_fileSystem.MapPath);
8992

9093
var poolConfig = new JsPoolConfig
9194
{
@@ -140,7 +143,7 @@ protected virtual void InitialiseEngine(IJsEngine engine)
140143
if (!_config.LoadReact && _scriptLoadException == null)
141144
{
142145
// We expect the user to have loaded their own version of React in the scripts that
143-
// were loaded above, let's ensure that's the case.
146+
// were loaded above, let's ensure that's the case.
144147
EnsureReactLoaded(engine);
145148
}
146149
}
@@ -171,6 +174,23 @@ private void LoadResource(IJsEngine engine, string resourceName, Assembly assemb
171174
/// <param name="engine">Engine to load scripts into</param>
172175
private void LoadUserScripts(IJsEngine engine)
173176
{
177+
if (_config.ReactAppBuildPath != null)
178+
{
179+
var manifest = ReactAppAssetManifest.LoadManifest(_config, _fileSystem, _cache, useCacheRead: false);
180+
foreach (var file in manifest.Entrypoints?.Where(x => x != null && x.EndsWith(".js")))
181+
{
182+
if (_config.AllowJavaScriptPrecompilation
183+
&& engine.TryExecuteFileWithPrecompilation(_cache, _fileSystem, file))
184+
{
185+
// Do nothing.
186+
}
187+
else
188+
{
189+
engine.ExecuteFile(_fileSystem, file);
190+
}
191+
}
192+
}
193+
174194
foreach (var file in _config.ScriptsWithoutTransform)
175195
{
176196
try
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using Newtonsoft.Json;
11+
12+
namespace React
13+
{
14+
internal class ReactAppAssetManifest
15+
{
16+
public Dictionary<string, string> Files { get; set; }
17+
public List<string> Entrypoints { get; set; }
18+
19+
public static ReactAppAssetManifest LoadManifest(IReactSiteConfiguration config, IFileSystem fileSystem, ICache cache, bool useCacheRead)
20+
{
21+
string cacheKey = "REACT_APP_MANIFEST";
22+
23+
if (useCacheRead)
24+
{
25+
var cachedManifest = cache.Get<ReactAppAssetManifest>(cacheKey);
26+
if (cachedManifest != null)
27+
return cachedManifest;
28+
}
29+
30+
var manifestString = fileSystem.ReadAsString($"{config.ReactAppBuildPath}/asset-manifest.json");
31+
var manifest = JsonConvert.DeserializeObject<ReactAppAssetManifest>(manifestString);
32+
33+
cache.Set(cacheKey, manifest, TimeSpan.FromHours(1));
34+
return manifest;
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)