0

What's the simplest way to "build" a static web page and Node backend such that the Node server runs in HTTPS in dev mode (but not production) and the static web page can point to https://localhost/foo in dev, but just /foo in production?

Long Story

This is probably a mess, and probably not The Right Way to do things, but this is my first time using React for anything other than an SPA, so I've never had to deal with Webpack/Babel (or any other build tools) before.

I have a static landing page, which has a couple of React components in it (i.e., it's not a SPA where it's all React, it's just loading React components to a few roots). One of them is for handling Stripe payments. There's also a simple back-end Node server to handle the Stripe payment-intent creation and web hooks that get registered with Stripe.

To simplify development, I've put this all in one repository, that looks like this:

repo/app.js
     devserver.js
     index.html
     package.json
     client/package.json
            webpack.config.js
            src/<react stuff>
     server/package.json
           /index.js
           /<other node stuff>
     public/<webpack output>

The index.html is the static page. The top-level app.js, devserver.js, and package.json really only exist for development, so I can load the static web page on localhost and watch for source changes on both the React and Node sides.

repo/package.json:

{
  "name": "landing-page",
  "version": "0.99.2",
  "description": "Static HTML for landing page",
  "private": true,
  "scripts": {
    "start": "npm-run-all build:dev:client -p start:* watch:client",
    "start:devserver": "nodemon devserver.js",
    "start:server": "cd server && npm start",
    "watch:client": "cd client && npm start",
    "build:dev:client": "cd client && npm run build:dev",
    ...

So, if I npm start, it

  1. builds the React stuff —

repo/client/package.json:

{
  "name": "landing-page-react",
  "version": "0.99.2",
  "description": "React components for landing page",
  "private": true,
  "main": "index.js",
  "homepage": ".",
  "scripts": {
    "build:dev": "webpack --config ./webpack.config.js --mode development",
    "start": "webpack --watch --config ./webpack.config.js --mode development",
    ...

and repo/client/webpack.config.js:

const path = require('path');
const Dotenv = require('dotenv-webpack');

module.exports = (env, argv) => {
  console.log('Webpack mode: ', argv.mode);

  const config = {
    entry: path.resolve(__dirname, './src/index.js'),
    plugins: [
      new Dotenv()
    ],
    module: {
      rules: [
        {
          test: /\.(js)$/,
          exclude: /node_modules/,
          use: ['babel-loader']
        }
      ]
    },
    resolve: {
      extensions: ['*', '.js', '.jsx']
    },
    output: {
      path: path.resolve(__dirname, '../public'),
      filename: 'bundle.js',
    },
    devServer: {
      contentBase: path.resolve(__dirname, '../public'),
    },
    mode: argv.mode
  }

  return config;
};

putting the output back up in repo/public whence it's included in index.html.

  1. Launches the Node server —

repo/server/package.json:

{
  "name": "landing-page-node-module",
  "version": "0.99.2",
  "description": "Process stripe payments for landing page",
  "private": true,
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "NODE_OPTIONS='--experimental-specifier-resolution=node --trace-warnings' nodemon index.js",

and the "dev" server, so I can actually see/use the index.html page:

repo/devserver.js:

const http = require('http');
const PORT = process.env.PORT || 3000;

const appServer = require('./app');
const httpServer = http.createServer(appServer);

httpServer.listen(PORT);
console.log(`Listening on port ${PORT}...`);

repo/app.js:

const path = require('path');
const express = require('express');

const app = express();
app.use(express.static(path.join(__dirname)));
app.use('/', express.static('index.html'));

module.exports = app;

This all already seems a little preposterously complex, but, it works. If I navigate to localhost:3000 it loads my index.html, which pulls in the React transpiled scripts from repo/public; that frontend communicates fine with the Node server running on localhost:9292; and if I make anyone changes to either the server side or the client side, it all gets "watched" and reloaded. So, so far so good.

Now the problem. I want to add logging both in the static web page and the React components, such that those logs go to the Node server.

In the index.html I've added

      <script>
          function sendToLog(data) {
              const body = JSON.stringify(data);

              // Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
              (navigator.sendBeacon && navigator.sendBeacon('/api/log', body)) ||
                  fetch('/api/log', {body, method: 'POST', kee  palive: true});
          }
      </script>

There are two problems with this. First, '/api/log' would work fine in production, but doesn't in dev, because it needs to go to a different port. So I need to "build" the static index.html such that it's '/api/log' for production, but 'localhost:9292/api/log' in dev. Second, I need the node server to run over https in dev, because sendBeacon requires that. Third, I'm hoping the React code will be able to rely on that sendToLog function because it's available in the parent page, but am prepared for the possibility that I may need to do some custom building of the React components as well.

In production, this is all behind a Apache, which is in turn behind a CDN. The CDN forces https and when the requests hit Apache, it just proxies any /api URLs to the node server on localhost over http, so ... everything Just Works. But there is no https (nor Apache) in my dev environment.

I started down a path of putting a bunch of if ('production' !== process.env.NODE_ENV code in the server to use a local CA and certs, but it occurs to me that this is not the right thing to do. It feels like the server should be built using the secure config when in dev, and not when in prod (because it's secured through other means, and why add the overhead). And, ideally, I'd like to use the same tool to build the static html file.

Given where I am, what's the simplest way to do this? Is this something that can reasonably/easily be done with Webpack and babel? Or do I need to add something else (I have no idea what, Gulp?) to my build process? And either way, any pointers on where to start configuring this?

philolegein
  • 1,099
  • 10
  • 28

1 Answers1

0

Long term, this entire thing will probably have to be redone (maybe as NextJS), and somewhere between where I am now and then, there are probably better ways to do this with WebPack and WebpackHtmlPlugin, but the simple way I ended up going was to use EJS from the command line in package.json to build the index.html:

npm i ejs --save-dev

package.json:

{
  "name": "seminar-landing-page",
  "version": "0.99.2",
  "description": "Static HTML for landing page",
  "private": true,
  "scripts": {
    ...
    "build:dev:views": "ejs views/index.ejs -f views/config/index.dev.json -o ./index.html",
  },

Then the code snippet from index.html becomes in repo/views/index.ejs:

  <script>
      function sendToLog(data) {
          const body = JSON.stringify(data);

          // Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
          (navigator.sendBeacon && navigator.sendBeacon('<%- api_server _%>/api/log', body)) ||
              fetch('<%- api_server _%>/api/log', {body, method: 'POST', keepalive: true});
      }
  </script>

and I have separate prod and dev config.json files, repo/views/config/index.dev.json:

{
    "api_server": "https://localhost:9292"
}

repo/views/config/index.prod.json:

{
    "api_server": ""
}

On the server side, I did end up going with an if, but I only ended up having to do it in one place:

import { readFileSync } from 'fs';
const PROD = ('production' === process.env.NODE_ENV);
if (PROD) {
  app.listen(PORT, () => logger.debug(`LISTENING AT PORT ${PORT}`));
} else {
  let https;
  try {
    https = await import('node:https');
  } catch (err) {
    logger.error('HTTPS support is required in dev and disabled in node');
    process.exit(1);
  }

    const key = readFileSync('./config/cert.key');
    const cert = readFileSync('./config/cert.crt');

    const server = https.createServer({ key: key, cert: cert }, app);

    server.listen(PORT, () => logger.debug(`LISTENING HTTPS AT PORT ${PORT}`));
}
philolegein
  • 1,099
  • 10
  • 28