Setting up Webpack to Modify URL per Environment in TypeScript

Setting up Webpack to Modify URL per Environment in TypeScript

A website I am currently creating has a very simple front end, but I wanted to be able to swap out instances of my API URLs in TypeScript depending which environment I am working in or building for. So far they are all ‘http://127.0.0.1:8000/’, but when I deploy, I didn’t want to have to remember to set a URL for prod or staging etc.

Unfortunately, I was unable to find a good way to do this with just the TypeScript compiler for static files. I’m not a big fan of the complexity that webpack adds, but I’ve noticed it also minifies and obfuscates my javascript, which is a nice bonus. If you know of a good way to do this with the TypeScript compiler alone, please let me know!

I tried a few different methods, but a combination of using webpack and splitting my config into development and production and DefinePlugin worked. Otherwise, I mostly just followed the installation instructions for webpack including installing locally. I tried installing globally based on other tutorials, but I ran into a bunch of issues doing that.

Also, I’m a little frustrated with how confusing the documentation was on this. I tried using Environment Variables in Webpack, but I couldn’t figure out how to access those in my code (not just the config file). It also took me some searching to understand how to use DefinePlugin because the documentation does not make it clear where that should go or how to include it. I found these two links that helped me figure it out (see my webpack.dev.js, webpack.staging.js, and webpack.prod.js files below).

With this setup, I run ‘npm run build:test’, ‘npm run build:staging’, or ‘npm run build:prod’ depending if I am working locally or building for production. Those commands are mapped in package.json:

{
  "name": "av_frontend",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build:test": "webpack --config webpack.dev.js --watch ",
    "build:staging": "webpack --config webpack.staging.js",
    "build:prod": "webpack --config webpack.prod.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/bootstrap": "^4.1.2",
    "@types/node": "^10.12.12",
    "bootstrap": "^4.1.3"
  },
  "devDependencies": {
    "ts-loader": "^5.3.1",
    "typescript": "^3.2.2",
    "webpack": "^4.27.1",
    "webpack-cli": "^3.1.2",
    "webpack-merge": "^4.1.4"
  }
}

I added --watch on test so that when I’m developing in VSCode, I can just leave it running and it’ll update whenever I save a file. If I want to run that manually I have to run ‘node_modules/.bin/webpack --config webpack.dev.js --watch’ because I installed webpack locally, not globally.

When I first got this set up, I realized it was only outputting a single Javascript file because I didn’t understand how ‘entry’ and ‘output’ worked in the webpack config file (see code below). Now I define each entry JS file for each page (they both have an import for my config with the IP addresses) with a name. Under ‘output’ [name].js corresponds with each ‘entry’. So my output ends up in the dist/ folder and the two files are named ‘login.js’ and ‘purchase.js’ based on the two fields in the ‘entry’ object. Any files added to 'entry' will produce a corresponding output Javascript file.

I also missed something in the instructions for Typescript when I was initially setting this up that lead to a long hunt for why it wasn’t including my config.ts file (the error messages were not great). Don’t forget the ‘resolve’ field below or webpack will get confused when trying to import any file with a .ts extension referenced in another file.

webpack.common.js

const path = require('path');

module.exports = {
  entry: {    
    login: './src/login.ts',
    purchase: './src/purchase.ts'   
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
};

webpack.dev.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'API_URL': JSON.stringify("http://127.0.0.1:8000/")
      }
    })
  ]
});

webpack.staging.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'API_URL': JSON.stringify("https://staging.com/")
      }
    })
  ]
});

webpack.prod.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'API_URL': JSON.stringify("https://production.com")
      }
    })
  ]
});

I wrote this blog post originally thinking I had solved this problem, but the solution I was using can only handle development and production environments. It also is a little more complicated than the above solution. It is described here: https://basarat.gitbooks.io/typescript/content/docs/tips/build-toggles.html