Skip to main content

Typescript DOM Webpack

TST, Hongkong

Setup

tsc --init
npm init -y
npm install lite-server

./tsconfig.json

{
  "include": "./src",
  "exclude": "./node_modules",
  "compilerOptions": {
    "target": "ES2015",
    "module": "ES2015", /* Specify what module code is generated. */
    "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
    "sourceMap": true, /* Create source map files for emitted JavaScript files. */
    "outDir": "./public", /* Specify an output folder for all emitted files. */
    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
    "strict": true, /* Enable all strict type-checking options. */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}

./package.json

{
  "name": "tsc-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.ts",
  "type": "module",
  "scripts": {
    "tsc": "tsc --watch",
    "start": "lite-server --baseDir='public'",
    "dev": "node public/index.js",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "lite-server": "^2.6.1"
  }
}
tree -L 2
.
├── public
│   ├── index.js
│   └── modules
├── node_modules
├── package.json
├── package-lock.json
├── src
│   ├── index.html
│   ├── index.tss
│   └── modules
│       ├── actions.ts
│       ├── events.ts
│       └── utils.ts
└── tsconfig.json

Provide a couple of dummy functions and classes in ./src/modules and import them into:

./src/index.ts

import Event from './modules/events.js'
import Action from './modules/actions.js'
import { add, multiply, subtract } from './modules/utils.js'

console.log('INFO :: Hello World!')

const processEvent = new Event(88, 'motion detected', 'pir sensor')
processEvent.notify()

const processAction = new Action(89, 'motion detected', 'alarm input', 'recording triggered')
processAction.triggerAction()

console.log(add(33, 77))
console.log(multiply(33, 77))
console.log(subtract(33, 77))

Run the tsc compiler and import the generated JS file into:

./public/index.html

<script type="module" src="index.js"></script>

Running the dev script as defined in package.json now executes all functions in the generated public/index.js:

npm run dev

> tsc-webpack@1.0.0 dev
> node public/index.js

INFO :: Hello World!
[ 88, 'motion detected', 'pir sensor' ]
INFO :: action alarm recording was triggered!
110
2541
-44

The same works inside a web browser by running the start script:

npm run start

> tsc-webpack@1.0.0 start
> lite-server --baseDir='public'

[Browsersync] Access URLs:
 --------------------------------------
       Local: http://localhost:3000
    External: http://192.168.2.112:3000
 --------------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 --------------------------------------
[Browsersync] Serving files from: public
[Browsersync] Watching files...
24.01.17 23:00:25 200 GET /index.html
24.01.17 23:00:25 200 GET /index.js
24.01.17 23:00:25 200 GET /modules/events.js
24.01.17 23:00:25 200 GET /modules/actions.js
24.01.17 23:00:25 200 GET /modules/utils.js
24.01.17 23:00:37 200 GET /
24.01.17 23:00:37 200 GET /index.js

All files are loaded successfully and the browser console output is identical to the terminal output from the previous step:

INFO :: Hello World! index.js:4:9
Array(3) [ 88, "motion detected", "pir sensor" ] events.js:10:17
INFO :: action alarm recording was triggered! actions.js:11:17
110 index.js:9:9
2541 index.js:10:9
-44 index.js:11:9

Adding NPM Modules

As a simple example install lodash to 'empower' your src/modules/utils.ts file:

npm install lodash
npm install --save-dev @types/lodash
import _ from 'lodash'

export function add(x: number, y:number) {
    return _.add(x, y)
}

export function multiply(x: number, y: number) {
    return _.multiply(x, y)
}

export function subtract(x: number, y: number) {
    return _.subtract(x, y)
}

Rerun the dev script to execute the script in Node.js and you should see that it executes just fine. Running the start script, however, leads to the following error message inside your browser:

Uncaught TypeError: The specifier “lodash” was a bare specifier, but was not remapped to anything. Relative module specifiers must start with “./”, “../” or “/”.

This can be resolved by using Webpack to bundle all dependencies, including imported npm modules, into a single JS file.

Webpack

Setup

npm install --save-dev webpack webpack-cli typescript ts-loader

./webpack.config.cjs

const path = require('path');

module.exports = {
  mode: "development", // production
  entry: './src/index.ts',
  devtool: 'inline-source-map', // remove in 'production'
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public'),
    publicPath: "/public"s
  },
};

In the previous test run all imports had to have the .js file extension. For Webpack the need to be removed:

Before:

import Event from './modules/events.js'
import Action from './modules/actions.js'
import { add, multiply, subtract } from './modules/utils.js'

After:

import Event from './modules/events'
import Action from './modules/actions'
import { add, multiply, subtract } from './modules/utils'

Now run the build script to run the bundler:

npm run build

> tsc-webpack@1.0.0 build
> webpack

asset bundle.js 1.39 MiB [emitted] (name: main)
runtime modules 1.25 KiB 6 modules
cacheable modules 533 KiB
  modules by path ./src/modules/*.ts 960 bytes
    ./src/modules/events.ts 342 bytes [built] [code generated]
    ./src/modules/actions.ts 412 bytes [built] [code generated]
    ./src/modules/utils.ts 206 bytes [built] [code generated]
  ./src/index.ts 476 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 531 KiB [built] [code generated]
webpack 5.89.0 compiled successfully in 1829 ms

Change to the HTML import to the following to use the newly generated bundled JS file and run the start script to verify that your browser is now able to find the imported lodash dependency:

./public/index.html

<script src="bundle.js"></script>

Webpack Dev-Server

Replacing the lite-server with the official webpack-dev-server:

npm install --save-dev webpack-dev-server

And replace the npm scripts accordingly:

./package.json

"scripts": {
    "tsc": "tsc --watch",
    "start": "webpack serve",
    "dev": "node public/bundle.js",
    "build": "webpack"
  }

And add the following configuration to:

./webpack.config.cjs

devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
      watch: true,
    },
    client: {
      overlay: {
        errors: true,
        warnings: false,
        runtimeErrors: true
      },
      logging: 'info', // 'log' | 'info' | 'warn' | 'error' | 'none' | 'verbose'
    },
    compress: true,
    port: 3000,
  }

The start script will now run the dev server on port 3000:

npm run start

> tsc-webpack@1.0.0 start
> webpack serve

<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:3000/
asset bundle.js 1.99 MiB [emitted] (name: main)
runtime modules 27.5 KiB 13 modules
modules by path ./node_modules/ 709 KiB
  modules by path ./node_modules/webpack-dev-server/client/ 71.8 KiB 16 modules
  modules by path ./node_modules/webpack/hot/*.js 5.3 KiB 4 modules
  modules by path ./node_modules/html-entities/lib/*.js 81.8 KiB
    ./node_modules/html-entities/lib/index.js 7.91 KiB [built] [code generated]
    + 3 modules
  ./node_modules/ansi-html-community/index.js 4.16 KiB [built] [code generated]
  ./node_modules/events/events.js 14.5 KiB [built] [code generated]
  ./node_modules/lodash/lodash.js 531 KiB [built] [code generated]
modules by path ./src/ 1.4 KiB
  ./src/index.ts 476 bytes [built] [code generated]
  ./src/modules/events.ts 342 bytes [built] [code generated]
  ./src/modules/actions.ts 412 bytes [built] [code generated]
  ./src/modules/utils.ts 206 bytes [built] [code generated]
webpack 5.89.0 compiled successfully in 2123 ms

And the browser console should show the results as before:

[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled. index.js:485
[HMR] Waiting for update signal from WDS... log.js:39
INFO :: Hello World! index.ts:5:8
Array(3) [ 88, "motion detected", "pir sensor" ] events.ts:15:16
INFO :: action alarm recording was triggered! actions.ts:12:16
110 index.ts:13:8
2541 index.ts:14:8
-44 index.ts:15:8

Production

npm install --save-dev clean-webpack-plugin html-webpack-plugin

Duplicate the Webpack configuration file and name those files:

  • webpack.dev.config.cjs
  • webpack.prod.config.cjs

Keep the development/not-production file as is and change the latter to:

./webpack.prod.config.cjs

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: "production",
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin()
  ],
  output: {
    filename: '[contenthash].bundle.js',
    path: path.resolve(__dirname, 'public')
  }
};

The clean webpack plugin will now delete all files inside the public directory. While the html plugin generates an HTML file in public that embeds bundle.js file that now contains hash in it's name to prevent cashing.

This configuration file can be referenced inside the build script:

./package.json

"scripts": {
    "tsc": "tsc --watch",
    "start": "webpack serve --config webpack.dev.config.cjs",
    "dev": "node public/bundle.js",
    "build": "webpack --config webpack.prod.config.cjs"
  }