Configure the create-react-app public URL post-build with Node.js and express
Tags: react, node, javascript, webdev
Published: 2020-03-29 11:56:54

**[Cover Art](https://www.pexels.com/de-de/foto/aufsicht-autobahn-baume-draufsicht-1624600/) by [Joshua Welch](https://www.pexels.com/de-de/@joshua-welch-763172)**


For a project, I had the requirement of making the public URL of the application configurable via an environment variable that can be set before starting the Node.js express server.

The frontend of that application is built with `create-react-app` which is very opinionated and has some limitations.

I will share with you those limitations and the workarounds for implementing such a feature.

**The Defaults**

create-react-app assumes that your application is hosted on the server root. E.g. the URL to your favicon in the build output `index.html` file would look similar to this the following:

```html
<link rel="shortcut icon" href="/favicon.ico"/>
```

In case you want to host your website under a relative part that is different from the server root there is an option for specifying the base URL either via the `homepage` key inside your `package.json` or the `PUBLIC_URL` environment variable that must be set before building the project. When running the `react-scripts build` script, the `%PUBLIC_URL%` placeholders inside the `index.html` file are replaced with the environment variable string.

In case we want to serve our application under a different public URL, such as  `https://my-site.com/app`, we can build the project like that:

```bash
PUBLIC_URL=https://my-site.com/app yarn react-scripts build
```

The contents of the build artifact `index.html` have now changed:

```html
<link rel="shortcut icon" href="https://my-site.com/app/favicon.ico"/>
```

**The limitations**

This method, however, has the drawback of requiring us to already know the public URL when building the frontend application.

As mentioned earlier our use-case requires setting the public URL dynamically, as the express server that is bundled as a binary and each user should be able to run that web server under a domain/path they specify.

**The solution**

The initial idea was to set `PUBLIC_URL` to some string that could get replaced by the express web server. The Node.js script loads the `index.html` file and replaces all the occurrences of the placeholder string:

```js
"use strict";

const express = require("express");
const app = express();
const path = require("path");
const fs = require("fs");

const PUBLIC_PATH = path.resolve(__dirname, "build");
const PORT = parseInt(process.env.PORT || "3000", 10)
const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;

const indexHtml = path.join(PUBLIC_PATH, "index.html");
const indexHtmlContent = fs
  .readFileSync(indexHtml, "utf-8")
  .replace(/__PUBLIC_URL_PLACEHOLDER__/g, PUBLIC_URL);

app.get("/", (req, res) => {
  res.send(indexHtmlContent);
});

app.use(express.static(path.join(PUBLIC_PATH)));

app.listen(PORT);
```

Now we can build our app like this:

```bash
PUBLIC_URL=__PUBLIC_URL_PLACEHOLDER__ yarn react-scripts build
```

However, this only solves linking the assets correctly. From an application point of view, we also need to figure out the application root path. Here is a quick example of our `Image` component:

```js
const Image = () =>
  <img src={`${process.env.PUBLIC_URL}images/me.jpeg`} />
```

Because we specified `PUBLIC_URL` being set to `__PUBLIC_URL_PLACEHOLDER__` and the environment variable is also embedded inside the JavaScript bundles (and used for resolving asset paths) the server will now send requests to `__PUBLIC_URL_PLACEHOLDER__/images/me.jpeg` 😅.

If we search for the string `__PUBLIC_URL_PLACEHOLDER__` inside the build assets located at `build/static/js` we can find multiple occurrences.

create-react-app injects an environment object inside the bundle that is similar to the Node.js `process.env` object.

```js
process.env = {
  NODE_ENV: "production",
  PUBLIC_URL: "__PUBLIC_URL_PLACEHOLDER__/"
}
```

In order to have a viable solution we also need to replace those occurrences on that object with the correct URL.

But parsing those `.js` files while serving them and replacing the string with express is not a good option as we now need to do it either on each request or cache the file contents in memory or in a separate file.

After some thinking, I realized there is a better option available that would allow us to only replace the `.js` content once post-build.

First, we add the following to our `index.html` file:

```html
<script>
  window.__PUBLIC_URL__ = "";
</script>
```

Make sure to add it into the head of the document to ensure it is loaded/evaluated before our application `.js` bundles.

Up next we must transform the `process.env` definition to the following:

```js
process.env = {
  NODE_ENV: "production",
  PUBLIC_URL: window.__PUBLIC_URL__ + "/"
}
```

We can achieve that by writing a script that will replace the occurrence of the `__PUBLIC_URL_PLACEHOLDER__` string inside our `build/static/js/*.js` files with `window.__PUBLIC_URL__`. That script can be executed immediately after running `yarn react-scripts build`.

I found a cool library [replacestream](https://www.npmjs.com/package/replacestream), that allows replacing file contents while streaming it. This keeps the memory footprint low for bigger application bundles.

```js
// scripts/patch-public-url.js
"use strict";

const fs = require("fs");
const path = require("path");
const replaceStream = require("replacestream");

const main = async () => {
  const directory = path.join(__dirname, "..", "build", "static", "js");
  const files = fs
    .readdirSync(directory)
    .filter(file => file.endsWith(".js"))
    .map(fileName => path.join(directory, fileName));

  for (const file of files) {
    const tmpFile = `${file}.tmp`;
    await new Promise((resolve, reject) => {
      const stream = fs
        .createReadStream(file)
        .pipe(
          replaceStream(
            '"__PUBLIC_URL_PLACEHOLDER__"',
            // the whitespace is added in order to prevent invalid code:
            // returnwindow.__PUBLIC_URL__
            " window.__PUBLIC_URL__ "
          )
        )
        .pipe(
          replaceStream(
            '"__PUBLIC_URL_PLACEHOLDER__/"',
            // the whitespace is added in order to prevent invalid code:
            // returnwindow.__PUBLIC_URL__+"/"
            ' window.__PUBLIC_URL__+"/"'
          )
        )
        .pipe(fs.createWriteStream(tmpFile));
      stream.on("finish", resolve);
      stream.on("error", reject);
    });
    fs.unlinkSync(file);
    fs.copyFileSync(tmpFile, file);
    fs.unlinkSync(tmpFile);
  }
};

main().catch(err => {
  console.error(err);
  process.exitCode = 1;
});

```

Let's also replace the `window.__PUBLIC_URL__` assignment inside our `index.html` within the Node.js code.

```diff
"use strict";

const express = require("express");
const app = express();
const path = require("path");
const fs = require("fs-extra");

const PUBLIC_PATH = path.resolve(__dirname, "build");
const PORT = parseInt(process.env.PORT || "3000", 10)
const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;

const indexHtml = path.join(PUBLIC_PATH, "index.html");
const indexHtmlContent = fs
  .readFileSync(indexHtml, "utf-8")
-  .replace(/__PUBLIC_URL_PLACEHOLDER__/g, PUBLIC_URL);
+  .replace(/__PUBLIC_URL_PLACEHOLDER__/g, PUBLIC_URL)
+  .replace(/window\.__PUBLIC_URL__=""/, `window.__PUBLIC_URL__="${PUBLIC_URL}"`);

app.get("/", (req, res) => {
  res.send(indexHtmlContent);
});

app.use(express.static(path.join(PUBLIC_PATH)));

app.listen(PORT);
```

Let's also adjust our build script inside the `package.json`:

```bash
PUBLIC_URL=__PUBLIC_URL_PLACEHOLDER__ react-scripts build && node scripts/patch-public-url.js
```

Post-build, we can start our server like this:

```js
PUBLIC_URL=http://my-site.com/app node server.js
```

**Bonus 🎁: NGINX Reverse Proxy Configuration**

```conf
upstream app {
    server localhost:3000;
}
server {
    listen       80;
    server_name  my-site.com;

    location /app {
        rewrite ^/app(/.*)$ $1 break;
        proxy_pass http://app/;
        # We also sue WebSockets :)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }
}
```

**Further note regarding service workers:**

In case you inspected the `build` folder and searched for `__PUBLIC_URL_PLACEHOLDER__`, you probably noticed that there are also service-worker `.js` files and also an `asset-manifest.json` file that includes the given string. I currently do not care about those, because our application has no offline mode. If you consider this, you will probably have to make some more string replacements.

Furthermore, since we are exclusively using CSS in JS I did not do any CSS string replacements. If you do so and use the `url()` you might also need to adjust your CSS files.

**We have finished 🎉.**

Do you have anything to add to that method, found a typo or got a better method for doing the same thing? Drop your comment and start a discussion below ⬇ 


**Thank you so much for reading!**