facebook / create-react-app

Set up a modern web app by running one command.
https://create-react-app.dev
MIT License
102.83k stars 26.88k forks source link

Runtime environment variables #2353

Open kmalakoff opened 7 years ago

kmalakoff commented 7 years ago

Regarding this pull request around the improvements to environment variables, based on @gaearon's suggestion, I wanted to start a discussion on how to handle a docker-centric, 12 factor app-based workflow where environment variables are provided externally at runtime rather than at build time so that the exact same assets can be run in multiple environments.

Constraints / design goals mentioned is:

It's important though to note that CRA always produces static bundles, and they are expected to work in any environment regardless of server, container, etc.

In the past, I have implemented the following two solutions:

1) render environment variables into the HTML and then hoist them into an application at initialization 2) fetch environment variables from a server (for example, before creating the store and rendering the app with some static content during the initial fetch)

The first solution's benefit is that there is no delay before initial render, but with create react app dynamically modifying the html file, it becomes a little more tricky to implement since you would need to parse or search / replace within the rendered html before serving the file.

I'm just wondering if there is a better / best way to provide runtime environment variables to CRA applications and if we can get agreement on an approach, if it can be integrated into the CRA pipeline.

Thank you for all the great work and hoping for something awesome here! :pray:

heyimalex commented 7 years ago

Environment variables have to be compiled in because things like NODE_ENV lead to tons of dead code elimination, so whatever solution you're looking for will probably need to coexist with env vars as they are today.

And I feel like you've hit the right solution: a script tag in the head of public/index.html that defines the variables, either inline or as an external js file. You've got the tradeoffs right as well; an extra request vs a slightly more complex release-creation process. Really not hard to do today, and in my opinion out of the scope of create-react-app. What's wrong with just rebuilding for configuration changes?

But if we're brainstorming...

Maybe env vars could somehow be denoted as being dynamic/runtime injected. A prefix of REACT_APP_RUNTIME or something. They get compiled into global variable references, something like window.runtimeEnvVars.<varname>, and instead of an index.html you get an index.html.template and a little script that can combine your partial build with a .env configuration into a full release.

kmalakoff commented 7 years ago

Interesting and totally understand if this might be out scope for CRA.

The main problem is that when I consult, I need to tell clients who deploy via docker and 12 factor app principles to ignore the environment variable solution in CRA which is a bit confusing since one would expect that CRA's environment variables solution would meet this common use case. To be honest, I'm actually not sure of why anyone would want build time environment variables, but it could be that I've been using and advocating for 12 factor app principles to clients for too long...

In your brainstorming ideas, is there a way to inject code into the CRA index.html at runtime currently? If not, that sounds like an interesting line of potential solutions.

pscanf commented 7 years ago

What's wrong with just rebuilding for configuration changes?

@heyimalex: for instance if you deploy your apps using docker, having to rebuild the app for configuration changes has some drawbacks:

Proposal

I suggest an approach similar to the one I used for sd-builder, a tool conceptually very similar to CRA that I've built back when CRA didn't yet exist (and that I would like to discontinue in favour of CRA).

sd-builder's approach

In apps built with sd-builder, configuration is loaded from a file - app-config.js - that you include in your app's index.html. The file defines the global variable window.APP_CONFIG, which holds the configuration object.

During development, the sd-builder dev server generates the file from the variables listed in the .env file.

sd-builder also provides a command - sd-builder config - to generate the file from (scoped) environment variables.

Creating the production bundle is therefore a two-step process:

When deploying with docker, you'd typically run the first step when building the docker image, and the second step when running the container.

This solves the last two drawbacks listed above. It doesn't solve the first one because you still need sd-builder to generate the config file, but that could be remedied by extracting the generation logic (30 lines of js) into a separate, smaller utility (maybe even with a C/bash port to cut down on runtime dependencies).

Suggestion for CRA

The above approach could be used in CRA without touching CRA. You add app-config.js in your index.html and develop a script that:

However this approach has some issues:

So I would suggest to bring that behaviour into CRA: in development, by making react-script start generate the config file and the configured index.html, in non-development, by providing a react-script config script that does it. (You could actually do without app-config.js, and simply embed the configuration in the index.html).


Thoughts? (@kmalakoff @gaearon @Timer)

heyimalex commented 7 years ago

Here's a build script to do this. Usage:

It's pretty hacky but should be a good proof-of-concept for how the experience could be.

hoolymama commented 7 years ago

I ended up here as was struggling with this too but since found a different solution. I'd be interested to know if it violates the any best practices, 12 step principles or if there are any security concerns.

Here's a gist

jakubknejzlik commented 7 years ago

@hoolymama thanks for the gist. I've been struggling with this problem in few projects and IMHO it's crucial to be able to specify environment variables for configuring the application.

Although it might not be in scope for CRA, it would be nice to unify some approach and be able to use it without and further setup needed.

adrianblynch commented 7 years ago

I solved this for my current project, which is ejected, by conditionally passing in config via /config/env.js:getClientEnvironment():

function getClientEnvironment(publicUrl) {
  // Code...
  NODE_ENV: process.env.NODE_ENV || "development",
  PUBLIC_URL: publicUrl,
  CONFIG: process.env.NODE_ENV === "development" ? config : "" // Only for development
  // Code...
}

This fixes it for local development.

I also inject config into a script block in index.html on the server (Express or Koa).

Then in my Config component I look for config on the window object OR in process.env.CONFIG.

I then control which config to use with CONFIG_ENV=ci|staging|whatever yarn dev|serve.

It's not perfect, but it fixes it for us and we can now promote our staging build to production on Heroku without rebuilding.

pscanf commented 7 years ago

Update from a user

For those who might be interested, I've built a static server "specialized" for serving and configuring at runtime create-react-app apps: staticdeploy/app-server. It also allows for an easy "dockerization" of the apps.

To allow runtime configuration, it just uses an alternative configuration mechanism. So it doesn't work out-of-the-box with create-react-app, but making it work is simply a matter of adding a script tag to public/index.html, so it's not too bad.

When serving the files, I also implemented some best-match redirects to allow the app base url (PUBLIC_URL) to be configured at runtime as well.

sr1994lu commented 7 years ago

How about rewriting withimmutable-js?

sazzer commented 6 years ago

My big concern with some of the solutions is - I'd like to be able to host my app on a CDN that only serves static files, whilst at the same time I'd like to know that the software deployed to Dev, QA, PreProd and Prod are all the same.

If I'm needing to dynamically generate files on startup, that fails the CDN requirement. If I'm needing to rebuild the entire application in order to deploy into different environments, that fails the QA/PreProd/Prod requirements.

The idea of having an app-config.js file that is deployed alongside the built application is the best I can think of so far. It means that the entire application is built once, and on deployment that file is added alongside it for deployment-specific details - such as backend URLs to call. It is another moving part to think about though, but not a very big one. It can probably be done in the Docker setup using just shell scripting as well, not needing a complex setup. Just a bunch of echo statements into a well-known filename passing in environment variables.

jakubknejzlik commented 6 years ago

@sazzer can you be more specific how does it fails the CDN requirement? Is it the recommendation of static content, which can be change just by changing the environment variables?

sazzer commented 6 years ago

@jakubknejzlik The CDNs that I've looked at serve up static files. They don't let you run server processes to dynamically generate content. This would mean that the files that are served to the browsers need to be the files that are uploaded, as-is.

What I'm really hoping for - but feeling less and less hopeful about - is a process similar to:

For the backend - which is a Java app - this is really simple. The Java process can be run with system properties on a per-environment basis to provide the appropriate config - database credentials right now - but the actual files deployed are identical from one environment to the next.

For the frontend, because it's CRA which produces static files, this is not so easy.

The best I've been able to come up with so far is for the frontend deployment to be:

This technically means that the deployment onto each environment is not the same, but it's as close as I can make it right now. This means that I can't guarantee that the deployment to Prod will work just because it passed all of the tests at the earlier stages. (And no, technically I can't guarantee that with the backend with its environment properties, but it's a lot easier to manage and verify that. Start service, call healthcheck endpoint, on failure rollback.)

gaearon commented 6 years ago

Is there anything we need to do on our side for this? I don't understand from reading this issue.

The-Loeki commented 6 years ago

Put bluntly: If we start a container/webserver w/some static js compiled through CRA, we would like e.g. PUBLIC_URL to be able to be set at runtime by the orchestration/webserver, not at build time.

josephfrazier commented 6 years ago

Hi everyone, I just wanted to chime in with my current workaround, adapted from some comments above (pscanf's and heyimalex's):

// clientEnv is generated this way because we don't want to expose all of process.env, // since it contains secrets (private API keys, etc) const clientEnv = require("react-scripts/config/env")().raw;

console.log( // Auto-generated by build-env.js, DO NOT EDIT window.process = { env: ${ JSON.stringify(clientEnv, null, 2) }}.trim())

* Add `    <script src="%PUBLIC_URL%/env.js"></script>` to the `<head>` of `public/index.html`
* If you're using a dynamic server, ensure it serves the `/env.js` path. For Express, this looks something like:
```js
app.get('/env.js', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'env.js'))
})

This has the same downsides @pscanf mentioned:

as well as the __process variable thing, but it works well enough for me.

The-Loeki commented 6 years ago

We set the PUBLIC_URL and some other stuffs we know are configurable to e.g. PUBLIC_URL_REPLACE_ME and do some good old fashioned sed to 'fix' it in the init script ;)

Very Dirty (c) but functional

heyimalex commented 6 years ago

@The-Loeki I think that potentially breaks sourcemaps and caching in weird ways. Even if you take out the service worker stuff, you have to remember to not set far future cache headers on /static/{css,js}/*, since your find-replace could make them not-so-static (which also means you're not getting the benefits of static asset caching). There are ways to fix this but they just add more complexity...

The-Loeki commented 6 years ago

We only serve the statically generated files, no nodejs & such; for PUBLIC_URL we traverse all builded files to fix them.

The one thing you have to be aware of regarding caching besides the headers is to not use relative URLs; they warn about that somewhere in the code.

heyimalex commented 6 years ago

@The-Loeki Everything in /static/ has a content hash appended to the filename so that you can safely set far-future caching headers. When you modify those files using sed, the contents change but the hashes don't. That means if you accidentally release with the wrong PUBLIC_URL, fix it and then release again, everyone who already got the bad build will continue to see that build until their cache expires or is cleared. Same idea with the service worker I think. Just something to consider!

EDIT: But thinking about it more, if PUBLIC_URL is wrong, the files won't be loaded in the first place. So maybe this is not actually an issue? 🤷‍♂️

The-Loeki commented 6 years ago

héhéhé I'm actually pretty sure that that is an issue, albeit a very minor one; our devs have had to force-reload their browsers regularly while moving the app around.

OTOH while I do strongly support it being a runtime configurable, it's not like it's going to move every day.

So thanks for pointing it out because we've been a bit suprised by the behaviour, but not overly bothered by it.

I might cook up some nasty-ass fix for it as well, but don't wait up for it ;)

SpacePotatoBear commented 6 years ago

I solved this using a hackish solution here https://stackoverflow.com/questions/51653931/react-configuration-file-for-post-deployment-settings

ideally there would be a clean native way (i.e import a js file that doesn't get bundled) but this is a good work around.

lirbank commented 6 years ago

@SpacePotatoBear I like your model/workaround. I currently do it based on the URL the client is accessed from - posted an example in your thread:

https://stackoverflow.com/a/51663697/1959584

alexvicegrab commented 6 years ago

A native way of having runtime environmental variables would be very helpful when deploying in Dockerised environments (e.g. Kubernetes) and following a 12-factor app approach, specifically https://12factor.net/build-release-run

shinebayar-g commented 6 years ago

Create React App runtime environment variables are much needed for containerized environment. Currently once its built there is no way to change environment variables. However there are some workaround like using additional config.js file and read from there. Even it's still file that need to be modified. In containerized environment everything should be setup by environment variables, not through some file. We've come very close to better solution. Idea is instead of reading from process.env, actually you can read from external url with get request. In response for get request there would be little nodejs server that returns its own process.env file's value which can be configured at runtime.

Note: I'm not JS developer, our guys hacked together this solution in our environment. Hope it helps someone.

jakubknejzlik commented 6 years ago

@shinebayar-g that's exactly where we've ended up too but in slightly different way. We created docker wrapper image that just during the container start creates js file with values from process.env ("REACTAPP*" only; assigned to window.ENV) and starts nginx. Not sure if it works, but maybe it could help someone :-) https://github.com/inloop/cra-docker

shinebayar-g commented 6 years ago

@jakubknejzlik I see, but your solution works when someone write their value into .env file itself right? If that's right, it's 1on1 scale right? But still big advantage is you don't have to rebuild image? In our case we had to use one base image for every other website. Also it has advantage of not writing anything into .env file itself but read it from ENVIRONMENT VARIABLEs.

jakubknejzlik commented 6 years ago

@shinebayar-g nope, this line: https://github.com/inloop/cra-docker/blob/master/entrypoint.sh#L1

is basically generating the env.js file with contents from env command and filters it for REACT_APP_*. This happens every time the container starts so it should contain the environment variables.

shinebayar-g commented 6 years ago

@jakubknejzlik , Just realized your genius idea haha. Your solution is actually instead of serving additional nodejs, you can actually do it on nginx itself, did I got it?

jakubknejzlik commented 6 years ago

Yes, you just update the file on startup and serve it as static file with plain nginx. Not sure if genius, but it works 😏

shinebayar-g commented 6 years ago

Today we completely dropped our additional nodejs server for in favor of nginx implementation of your example. With little modification in our end, it worked like a charm! I think this is current best work around for runtime environment variable. Thanks for docker image and scripts.

bjrb20 commented 5 years ago

@jakubknejzlik When I try to run your entrypoint.sh it outputs a "env.js?" Any idea what might be happening?

koshkarov commented 5 years ago

@SpacePotatoBear I like your model/workaround. I currently do it based on the URL the client is accessed from - posted an example in your thread:

https://stackoverflow.com/a/51663697/1959584

That is a very simple and elegant solution. Thank you for sharing. Implemented in 5 minutes. But in my case the environment URL is also a deployment variable, so I just added extra JS file (let's say 'env.js') to the code which is an access point to the the run-time config which identifies the environment (with process.env.NODE_ENV) and exports related configuration. So I just import env.js wherever I need config variables and use it.

jakubknejzlik commented 5 years ago

@BenRedmondBenham the entrypoint is supposed to be run as entrypoint in docker container. Checkout this repository where we use this image/approach:

https://github.com/graphql-services/graphql-credentials-admin/tree/develop?files=1

stuartsan commented 5 years ago

My view is that probably the cleanest way to achieve this kind of JS bundle portability across environments, in a docker-centric and 12-factor app kinda workflow, is some variation of the first item mentioned by @kmalakoff:

  1. render environment variables into the HTML and then hoist them into an application at initialization

And the general technique for getting there suggested by @jakubknejzlik:

echo "window.ENV = `jo \\`env | grep REACT_APP_\`end=1`" > env.js

Is great.

I like the idea of modifying the HTML page instead of modifiying or adding a JS file, because this avoids the problems with caching and the content hash mentioned by @heyimalex.

Example

I'm going to use a different prefix, JS_BUNDLE_RUNTIME_CONFIG_, so that it's obvious that these are different than the REACT_APP_ -prefixed environment variables that are interpolated at build-time.

Set config values in environment variables

export JS_BUNDLE_RUNTIME_CONFIG_API_URL="api.dogs.com"
export JS_BUNDLE_RUNTIME_CONFIG_ANALYTICS_URL="analytics.dogs.com"

Put those environment variables into a JSON string

# requires https://github.com/jpmens/jo
PREFIX="JS_BUNDLE_RUNTIME_CONFIG_"
CONFIG_JSON=$(jo $(env | grep "$PREFIX" | cut -c $(expr ${#PREFIX} + 1)-))

echo $CONFIG_JSON
# {"ANALYTICS_URL":"analytics.dogs.com","API_URL":"api.dogs.com"}

Templatized public/index.html

<body>
    <div id="root"></div>
    <script type="application/json" id="js-bundle-runtime-config">
      {{ JS_BUNDLE_RUNTIME_CONFIG }}
    </script>
</body>

Note: I've stuffed the values into a non-JS script tag, to avoid polluting the window with additional variables, and we'll parse the JSON from within the bundle. But as variables on the window would be fine too!

Inject the config

sed -i "s/{{ JS_BUNDLE_RUNTIME_CONFIG }}/$CONFIG_JSON/" build/index.html

WHEN does this happen? If you're in a docker container, right when the container starts up. But to @sazzer's point about CDNs, if you're just sending the static files off someplace, you could also do this after the build, at the moment of deployment.

What gets sent to the browser

<body>
    <div id="root"></div>
    <script type="application/json" id="js-bundle-runtime-config">
      {"ANALYTICS_URL":"analytics.dogs.com","API_URL":"api.dogs.com"}
    </script>
    <script src="my-bundle.min.js"></script>
</body>

Consume the config from inside your bundled source

function getRuntimeConfig() {
    return JSON.parse(
      document.getElementById('js-bundle-runtime-config').innerHTML
    );
}

const config = getRuntimeConfig();

console.log(config);
// {"ANALYTICS_URL":"analytics.dogs.com","API_URL":"api.dogs.com"}

Summary

The bundle is the exact same artifact across environments, but the HTML that references the bundle gets tweaked per-environment.

This comment already has become quite long, but I wrote a more detailed post on the topic also discussing how to deal with local development (TLDR: webpack string-replace-loader to inject the JSON).

One final thought: I don't think providing this kind of solution really falls under the responsibility of create-react-app, but I also totally agree with @kmalakoff in that I don't understand what use cases exist where it'd be beneficial to use the build-time environment variable interpolation mechanism compared to something like this. Is there anything I'm missing? (I also get that the build-time environment variable stuff isn't gonna go away, just wondering! :D )

heyimalex commented 5 years ago

I think your solution works well.

The use case for build time environment variables is that their values can be used during optimization. For example, all code wrapped in if (NODE_ENV === “development”) blocks is actually removed from the bundle in a production build. This is why React development builds are significantly slower than production builds.

stuartsan commented 5 years ago

@heyimalex, I should have been more specific: I don't get when it'd be more beneficial to use the mechanism of REACT_APP_-prefixed environment variables at build time, for the use case described by the docs as:

displaying information conditionally based on where the project is deployed or consuming sensitive data that lives outside of version control.

kunokdev commented 5 years ago

Some time ago, I've written detailed blog tutorial which solves this issue:

https://medium.freecodecamp.org/how-to-implement-runtime-environment-variables-with-create-react-app-docker-and-nginx-7f9d42a91d70

developerdino commented 5 years ago

For anyone wrestling with this problem I have just created a helper package on NPM to make the process a little easier. It has some scripts and documentation on how to use it. However, I would really love some feedback and any PRs are welcome if you think this is a useful package.

https://www.npmjs.com/package/@ethical-jobs/dynamic-env https://github.com/ethical-jobs/dynamic-env

P.S. kudos to @kunokdev as this is based on his blog article above.

andrewmclagan commented 5 years ago

Ok, even easier then that:

We created a repository / Docker image that generates a env.js file from your environment variables as per the CRA docs.

See: https://github.com/beam-australia/create-react-env

Uses a small Golang binary to build your env vars into JS. Based on the alpine-linux nginx image. Comes in at only 10mb. Now you can statically run your CRA and have runtime env vars!

developerdino commented 5 years ago

Definitely use @andrewmclagan implementation over the one I created. It is a much better implementation.

andrewmclagan commented 5 years ago

https://medium.com/@andrewmclagan_64462/runtime-environment-variables-create-react-app-84f7c261856c

amr commented 5 years ago

Just wanted to share my experience in case it helps someone. My specific case is that I'm trying to containerize my react app and my requirements are:

  1. I need to use nginx or other http servers I choose, I can't be tied to serving using node
  2. I need to be able to change my deployment env vars, restart the container/pod/etc and my changes should be picked up

After experimenting with some of the solutions here and elsewhere I come to believe that I actually don't require anything else from CRA -- processing the env vars at build time is the right thing to do and I should embrace that instead of trying to workaround it.

I don't know the other use cases but for me what I needed was in the context of being able to containerize my app and be able to change its env vars at deployment/env side.

So here is what I'm thinking of. Basically, I'd have a standard Dockerfile, the twist is that I'd only install the needed dependencies but it would not build the project yet, the custom entrypoint.sh would take care of that:

FROM node:12.2.0-alpine

# Copy project, install dependencies, etc, but do NOT build yet
# ...

RUN chmod +x entrypoint.sh
ENTRYPOINT [ "entrypoint.sh" ]

I didn't complete entrypoint.sh but its comments should tell you where I'm going with this:

#!/bin/sh
# This is what I have now, but just to get the entrypoint script going, I'd replace it
# with implementation of the comments below
yarn --silent clean:all && yarn --silent build
yarn --silent start

# What this needs to do in the future is
# 1. Capture REACT_APP_* env vars
# 2. Hash them and compare to their previous hash, if they differ:
#   a. Clean and rebuild
#   b. Store the new hash
# 3. Serve using nginx/etc

The disadvantage I see in this approach is that the image size is larger than if we just bundle the build using e.g. a multi-stage build. It also means the container maintains state (the env vars hash in entrypoint) but I think it's a caching mechanism, deleting the state won't do much harm. I'm still toying with this and didn't actually use it in production yet. Any feedback welcome!

jakubknejzlik commented 5 years ago

@amr we’ve been using this way too. But with rising number of project the cost of initial resources to even bring up the app was too high. It’s worth saying that we are not using the SSR so running application is matter of few megabytes of ram and fraction of cpu. Also startup times are <5s. In case of downloading dependencies and building the project we needed 1-2CPUs, ~2GB or ram and initial boot around ~5minutes...which was not acceptable (especially when using stuff like kubernetes properly).

amr commented 5 years ago

@jakubknejzlik the optimization of building only when env vars change (outlined above) should mitigate that a lot, because it would mean booting is very fast except the first time after you change env vars

jakubknejzlik commented 5 years ago

@amr no to be honest. We had a lot of commits and keeping the build phase in CI and not in production cluster felt more resource wise :).

andrewmclagan commented 5 years ago

Honestly we were suffering from building at run-time also. Our create-react-app pods in our Kubernetes cluster required huge resources to build.... but almost nothing to run. So it was costing us a ton more then was needed. This is why we wrote: https://github.com/beam-australia/react-env it builds the environment when your app or container starts. Populating a window._env object with your white listed REACT_APP_FOO env vars. You can then access them directly or use a provided helper:

import React from 'react'
import env from '@beam-australia/react-env'

// Using the helper function:

const Example1 = () => <div>{env('FOO')}</div>

// Access the entire whitelisted environment object:

const Example2 = () => <div>{JSON.stringify(env())}</div>

// Or directly access a env var:

const Example3 = () => <div>{window._env.REACT_APP_FOO}</div>

If you are using Next.js react-env also supports server rendering. Your env vars will be accessible through the env() helper client and server-side. There are examples in the repo.

There is currently a PR to update the documentation on this approach.

viet-wego commented 5 years ago

Thank @josephfrazier & @jakubknejzlik 🙇 I combine your solutions with tiny customization to make it work for me. Now I can be happy with Reactjs on my k8s cluster.

dmetzler commented 5 years ago

Thanks all for these solutions. Based on one of them, I've built a small tool in Go that creates an env-config.js based on the withe listed variable in .env file. It allows building a distroless Docker image (24MB) that embeds the whole mechanism.

I also wrote a blog post to explain how it works. Thanks again to @jakubknejzlik for the initial source of inspiration :-)

caub commented 5 years ago

It's really annoying that react-scripts can't allow a dynamic public/index.html.js template function

Here are the 2 ways I've found to have a dynamic index.html template:

The first one is quick-and-dirty, using a regexp and build/index.html, example in your server.js:

const indexHtml = fs.readFileSync('./build/.index.html', 'utf-8');
const m = indexHtml.match(/<\!--placeholder-->/);

app.get('/*', (req, res) => {
    const html = `${indexHtml.slice(0, m.index)}${/* inject whatever here */}${indexHtml.slice(m.index + m[0].length)}`
    res.send(html);
});

Seconds way, you would eject or use a custom webpack config, then customize HtmlWebpackPlugin to do this: in webpack.config.prod.js

        new HtmlWebpackPlugin({
            templateParameters: (compilation, assets) => {
                // write to disk on the root level a ._assets.json file, used by server.js
                // it's similar to build/asset-manifest.json, except only js/css and more importantly in loading order
                // https://github.com/jantimon/html-webpack-plugin/issues/982#issuecomment-398665019
                fs.writeFileSync('._assets.json', JSON.stringify(assets));
            },
            template: './index.html.js',
            filename: 'index.html',
            inject: false
        }),

in webpack.config.dev.js

        new HtmlWebpackPlugin({
            template: './index.html.js',
            filename: 'index.html',
            inject: true,
            foo: process.env.FOO
        }),

index.html.js

module.exports = ({
    foo, // just for example
    js = [],
    css = []
} = {}) => `<!DOCTYPE html>
<html lang="en">
<head>
  ...
  ${css.map(url => `<link href="${BASE_PATH + '/' + url}" rel="stylesheet">`).join('')}
</head>
<body>
  ....
  ${js.map(url => `<script src="${BASE_PATH + '/' + url}"${nonce}></script>`).join('')}
</body>
</html>`;

then in server.js

const indexHtml = require('./index.html'); // your index.html templating function
const assets = require('._assets.json');

// ....
app.get('/*', (req, res) => {
    const html = indexHtml({
        foo: 'bar',
        ...assets // contains {jsAssets: Array<String>, cssAssets: Array<String>}
    });
    res.send(html);
});

but both ways are not easy as you can see

After reading comments up there, I think the public/env.js approach is short, but more limited (you can't template non-js things, like a <link ... href="${dynamicUrl}">

andrewmclagan commented 5 years ago

you really dont need one.