As we saw in the previous episode that managing the project with the TypeScript is difficult and understanding modules can be difficult if we are not able to compile it properly. So we say with TypeScript either we can output individually compiled JS files or output everything into a single file with compiler option, module, set to AMD or System

Target

Let us create a simple configuration that can output server.js and client.js, one for running our server and one for getting included on our client end.

Webpack 4

Webpack has moved to almost zero configuration with the launch of version 4 but, that would not help us as we would need to handle .ts TypeScript extension with docker.

So let us start by installing webpack and webpack-dev-server and thereafter creating a simple webpack.config.js at our root folder.

npm i webpack webpack-cli

contents of webpack.config.js as below:

const path = require('path');

const isProduction = typeof NODE_ENV !== 'undefined' && NODE_ENV === 'production';
const mode = isProduction ? 'production' : 'development';
const devtool = isProduction ? false : 'inline-source-map';
module.exports = [
  {
    entry: './src/client.ts',
    target: 'web',
    mode,
    devtool,
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/,
          options: {
            compilerOptions: {
              "sourceMap": !isProduction,
            }
          }
        }
      ]
    },
    resolve: {
      extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
      filename: 'client.js',
      path: path.join(__dirname, 'dist', 'public')
    }
  },
  {
    entry: './src/server.ts',
    target: 'node',
    mode,
    devtool,
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/
        }
      ]
    },
    resolve: {
      extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
      filename: 'server.js',
      path: path.resolve(__dirname, 'dist')
    },
    node: {
      __dirname: false,
      __filename: false,
    }
  }
];

You can get the official reference for setting up TypeScript with WebPack at this link: https://webpack.js.org/guides/typescript/ as you can see we are using ts-loader for loading files with extension .ts & .tsx. We can also use awesome-typescript-loader for the same purpose. I prefer to go with ts-loader as mentioned in WebPack docs.

So let us install ts-loader as well:

npm i ts-loader

Also, have a look at the following section in our compiler for server.js

node: {
  __dirname: false,
  __filename: false,
}

We informed node to provide us with variable __dirname and __filename inside the compiled file.

Let us change the folder structure and naming of the files to make more sense.

  • ename src/index.ts to src/client.ts
  • Let the output folder for the compiled server.ts be <project-root>/dist
  • Let the output folder for the compiled client.ts be <project-root>/dist/public so that we do not expose our server.js
  • Make changes to our server.js accordingly to server static content from <project-root>/dist/public
  • Update our package.json scripts to compile server & client and watch them for changes and server accordingly.

Update the scripts in package.json to below:

"scripts": {
  "start": "webpack --colors && concurrently -r -n \"Webpack,Nodemon\" \"npm run webpack-watch\" \"npm run server-watch\"",
  "webpack-watch": "webpack --watch --colors",
  "server-watch": "nodemon --watch dist/server.js dist/server.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

What we are doing is: We are first simply compiling with WebPack so we have an Initial server.js and client.js to work with, then we start concurrently WebPack and Nodemon to watch changes over our application and restart accorindly.

This configuration give us our required output as below:

Which is beautiful, we Included component/global.ts and output the name inside the global namespace i.e. “Tirth”.

Code Splitting

The above is good is for novice projects but, In real life, we want to implement code splitting and make our app robust. Let us implement that! After TypeScript 2.4 dynamic import are available but with the inclusion of ESNext as the part of the lib option in compilerOptions.

Updating our tsconfig.json as below:

{
  "compilerOptions": {
    "noEmitOnError": true,
    "target": "es5",
    "lib": ["DOM", "ESNext", "DOM.Iterable", "ScriptHost", "es2015.promise"],
    "outDir": "dist"
  },
  "include": [
    "src/**/*"
  ]
}

Also to make our code work we would need to include respective polyfills, for now as let us simply include the polyfill.js from polyfill.io in our HTML inside our server.ts.

Dynamic import makes use of Promise so we the browser must have support for Promise or a polyfill must be added.

Contents of server.ts

import express = require("express");
const path = require("path");

const app: express.Application = express();
const port: number = 3000;

app.use("/static", express.static(path.join(__dirname, 'public')));

app.get('/', (req: express.Request, res: express.Response): express.Response => {
  return res.send(`<!DOCTYPE html>
    <html>
      <head>
        <title>My experiments with TypeScript</title>
        <!---- ADDING POLYFILL -->
        <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
        <style>
          html,body {
            margin:0;
            padding:0;
            font-family: monospace;
            font-size: 20px;
          }
          #logger {
            background-color: #eee; 
            padding: 20px; 
            max-width: 100%; 
            height: 600px; 
            max-height: calc( 90vh - 2em - 35px); 
            margin: 10px;
            overflow-y: auto;
          }
          .log-entry {
            max-width: 100%;
            padding: 5px;
            background-color: #f6f6f6;
          }
          .title {
            margin: 10px;
          }
        </style>
      </head>
      <body>
        <h1 class="title">Logger:</h1>
        <div id="logger"></div>
        <script src="/static/client.js" async></script>
      </body>
    </html>
  `);
});
app.listen(port, (): void => console.log(`Example app listening on port ${port}!`));

So basically we want our code split, which is no big of a deal with the WebPack. Webpack and TypeScript do support dynamic import but only when the module option is set to “esnext“. So we will need to update three files in this case:
1. tsconfig.json
2. webpack.config.js
3. server.ts

Add compilerOption module as “esnext

{
  "compilerOptions": {
    "noEmitOnError": true,
    "target": "es5",
    "module": "esnext",
    "lib": ["DOM", "ESNext", "DOM.Iterable", "ScriptHost", "es2015.promise"],
    "outDir": "dist"
  },
  "include": [
    "src/**/*"
  ]
}

In webpack.config.js we would need to inform WebPack that it should optimize with “splitChunks” and update output & entry as below:

    entry: {
      client: './src/client.ts'
    },
    output: {
      filename: (chunkData) => {
        // do not change the chunk name as of now as we are importing client.js in server.ts
        return chunkData.chunk.name === 'client' ? '[name].js': '[name].[chunkhash].js';
      },
      // We have the prefix /static/ thus adding public path for generated chunks
      publicPath: '/static/',
      // Chunkhash
      chunkFilename: '[chunkhash].js',
      path: path.join(__dirname, 'dist', 'public')
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
        minSize: 0
      }
    },

Other configurations remain as it is.

As we have changed the compilerOption for module to “esnext” our code
import express = require won’t work any more thus we will have to update our code to following:

import * as express from 'express';

That is all for our code splitting. Let us test it out and update our client.ts to following:

let str: string = "Hello, World!";
function log(s: any): void {
  const logger: HTMLElement = document.getElementById("logger");
  const logWrapper: HTMLElement = document.createElement("div");
  logWrapper.className = "log-entry";
  logWrapper.innerHTML = `${(new Date).toLocaleTimeString()}: (${typeof s}):: ${JSON.stringify(s)}`;
  logger.appendChild(logWrapper);
}
log(str);

setTimeout(() => {
  import('./component/global').then(Global => {
    log(Global);
  });
}, 2000);

So we are requesting for Global component after 2 seconds, which should load the chunk after 2 seconds and log whatever is loaded.

Which is pretty cool!

PS: The above tutorial is not production-ready and should be used for experimenting and learning purpose only!

Reference to previous blog series/episodes:

The code base of this episode, Part 3.2, is available at:
https://github.com/Atyantik/typescript-series/tree/part-3.2