Skip to content

Commit b7b1644

Browse files
committed
Fix PUBLIC_URL handling
1 parent f937421 commit b7b1644

File tree

14 files changed

+17234
-134
lines changed

14 files changed

+17234
-134
lines changed

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
# react-scripts-with-ssr
2-
3-
[![Build Status](https://travis-ci.org/joernb/react-scripts-with-ssr.svg?branch=master)](https://travis-ci.org/joernb/react-scripts-with-ssr)
1+
# react-scripts-with-ssr [![Build Status](https://travis-ci.org/joernb/react-scripts-with-ssr.svg?branch=master)](https://travis-ci.org/joernb/react-scripts-with-ssr)
42

53
This is a fork of [react-scripts](https://github.com/facebook/create-react-app/tree/master/packages/react-scripts), which adds support for server-side rendering (SSR).
64

@@ -32,6 +30,8 @@ npm run serve
3230

3331
## How it works
3432

33+
### SSR entry point
34+
3535
The script will generate an additional entry point for server-side rendering in `src/index.ssr.js` (or `src/index.ssr.tsx`). It exports an express-style request handler, that looks like this:
3636

3737
```js
@@ -47,6 +47,25 @@ During development, the request handler will be integrated into the local webpac
4747

4848
If `src/index.ssr.js` exports a function called `devServerHandler`, it will be invoked and its return value will be used as a request handler during development instead. This gives your entry point the possibility to make environment-specific adjustments. For example, all assets are compiled in-memory during development and should not be served from the real file system.
4949

50+
### Relative Urls
51+
`create-react-app` provides a variable called `PUBLIC_URL`, which is accessible via `process.env.PUBLIC_URL` in JavaScript or by using the placeholder `%PUBLIC_URL%` in Html.
52+
53+
For non-ssr projects, the public url has to be known at **compile time**. It is defined via Webpack's `publicPath` property and baked into the client-side js file with the `DefinePlugin` which hardcodes `process.env` into the compiled script.
54+
55+
For ssr projects however, it is possible to configure the PUBLIC_URL as environment variable on the server-side and render them into the client-side output during **runtime** instead. This makes the compiled code location-independent and you don't need to recompile the code if you deploy it to another location. To make this possible, `react-scripts-with-ssr` makes some significant changes:
56+
* The `public/index.html` template is supposed to contain a `<base href="%PUBLIC_URL%/" />` tag
57+
* Urls to other included files in `public/index.html` may be relative and will work in combination with the base tag
58+
* The variable substitution of `%PUBLIC_URL%` must take place during runtime inside the ssr request handler.
59+
* On the client-side, the base tag href value is provided as `process.env.PUBLIC_URL`
60+
* Other environment variables (NODE_ENV and the ones with REACT_APP prefix) are still baked in at compile time
61+
62+
### Transferring ssr data to the client
63+
64+
If you need to "transfer" values from the ssr request handler to the client (e.g. for preloaded state), you can do this:
65+
* Add a variable assignment script `<script>FOO='%FOO%'</script>` to `public/index.html`
66+
* Replace the placeholder in the ssr request handler with `.replace(/%FOO%/g, foo)`
67+
68+
5069
## Contribute
5170

5271
### Merge react-scripts updates

config/webpack.ssr.config.js

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,77 @@
22

33
const webpack = require('webpack');
44
const HtmlWebpackPlugin = require('html-webpack-plugin');
5+
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
56
const webpackConfigFactory = require('./webpack.config.js');
67
const paths = require('./paths');
8+
const getClientEnvironment = require('./env');
9+
const env = getClientEnvironment('');
710

811
// decorate original webpack config
912
module.exports = function (webpackEnv) {
1013
const template = webpackConfigFactory(webpackEnv);
11-
return Object.assign(
12-
// multi compiler config (client + server)
13-
[
14-
// ssr compiler config
15-
{
16-
...template,
17-
name: 'ssr',
18-
target: 'node',
19-
entry: [
20-
// ssr entry point, usually s.th. like `./src/index.ssr.tsx` with `export default (req,res) => {}`
21-
paths.appIndexSsrJs
22-
],
23-
output: {
24-
...template.output,
25-
filename: 'ssr.js',
26-
libraryTarget: 'commonjs2',
27-
// avoid using absolute path '/' defined by template
28-
publicPath: ''
29-
},
30-
optimization: {
31-
...template.optimization,
32-
// disable chunk splitting
33-
splitChunks: {},
34-
runtimeChunk: false
35-
},
36-
// filter out some plugins
37-
plugins: template.plugins.filter(plugin =>
38-
[
39-
// do not generate an additional index.html for ssr
40-
HtmlWebpackPlugin,
41-
// Avoid shadowing of process.env for ssr handler
42-
webpack.DefinePlugin
43-
].every(pluginClass => !(plugin instanceof pluginClass))
44-
),
45-
// do not bundle express
46-
externals: [
47-
"express"
48-
]
14+
// multi compiler config (client + server)
15+
return [
16+
// ssr compiler config
17+
{
18+
...template,
19+
name: 'ssr',
20+
target: 'node',
21+
entry: [
22+
// ssr entry point, usually s.th. like `./src/index.ssr.tsx` with `export default (req,res) => {}`
23+
paths.appIndexSsrJs
24+
],
25+
output: {
26+
...template.output,
27+
filename: 'ssr.js',
28+
libraryTarget: 'commonjs2',
29+
// use '' instead of '/' to make urls relative to base href
30+
publicPath: ''
4931
},
50-
// client compiler config
51-
{
52-
// use original config
53-
...template,
54-
name: 'client'
32+
optimization: {
33+
...template.optimization,
34+
// disable chunk splitting
35+
splitChunks: {},
36+
runtimeChunk: false
5537
},
56-
],
57-
// workarounds
38+
// filter out some plugins
39+
plugins: template.plugins.filter(plugin =>
40+
[
41+
// do not generate an additional index.html for ssr
42+
HtmlWebpackPlugin,
43+
// avoid shadowing of process.env for ssr handler
44+
webpack.DefinePlugin
45+
].every(pluginClass => !(plugin instanceof pluginClass))
46+
)
47+
},
48+
// client compiler config
5849
{
59-
// at least one script expects to find `config.output.publicPath` but `config` is now an array
50+
// use original config
51+
...template,
52+
name: 'client',
6053
output: {
61-
publicPath: template.output.publicPath
62-
}
63-
}
64-
);
54+
...template.output,
55+
// use '' instead of '/' to make urls relative to base href
56+
publicPath: ''
57+
},
58+
// filter out some plugins
59+
plugins: template.plugins.filter(plugin =>
60+
[
61+
InterpolateHtmlPlugin,
62+
// filter out original DefinePlugin
63+
webpack.DefinePlugin
64+
].every(pluginClass => !(plugin instanceof pluginClass))
65+
).concat([
66+
// redefine process.env values on the client
67+
new webpack.DefinePlugin({
68+
'process.env': {
69+
// use existing values
70+
...env.stringified['process.env'],
71+
// override with base href
72+
PUBLIC_URL: 'document.getElementsByTagName("base")[0].href'
73+
}
74+
}),
75+
])
76+
},
77+
];
6578
};

config/webpackDevServer.ssr.config.js

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,27 @@ module.exports = function(proxy, allowedHost) {
2424

2525
// fetch ssr handler after every compilation
2626
multiCompiler.hooks.done.tap('webpackDevServer.ssr', () => {
27-
// fetch ssr entry point file name
28-
const filename = path.resolve(paths.appBuild, "..", "dist", "ssr.js");
29-
// read code from in memory fs
30-
const code = compiler.outputFileSystem.readFileSync(filename).toString();
31-
// compile code to node module
32-
const exports = requireFromString(code, filename);
27+
// make errors inside hook visible
28+
try {
29+
// fetch ssr entry point file name
30+
const filename = path.resolve(paths.appBuild, "..", "dist", "ssr.js");
31+
// read code from in memory fs
32+
const code = compiler.outputFileSystem.readFileSync(filename).toString();
33+
// compile code to node module
34+
const exports = requireFromString(code, filename);
3335

34-
if (exports.devServerHandler) {
35-
// install dev server handler
36-
ssrHandler = exports.devServerHandler(compiler);
37-
} else if (exports.default) {
38-
// install production handler
39-
ssrHandler = exports.default;
40-
} else {
41-
// no handler found
42-
throw new Error("SSR entry point does not export a handler.");
36+
if (exports.devServerHandler) {
37+
// install dev server handler
38+
ssrHandler = exports.devServerHandler(compiler);
39+
} else if (exports.default) {
40+
// install production handler
41+
ssrHandler = exports.default;
42+
} else {
43+
// no handler found
44+
throw new Error("SSR entry point does not export a handler.");
45+
}
46+
} catch (error) {
47+
console.error(error);
4348
}
4449
});
4550

0 commit comments

Comments
 (0)