Post

14. Build Tools and Bundlers

πŸ”§ Master TypeScript build tools! Learn Webpack, Vite, esbuild, Rollup, and Babel for fast development and optimized production builds. ✨

14. Build Tools and Bundlers

What we will learn in this post?

  • πŸ‘‰ TypeScript with Webpack
  • πŸ‘‰ TypeScript with Vite
  • πŸ‘‰ TypeScript with esbuild
  • πŸ‘‰ Rollup for TypeScript Libraries
  • πŸ‘‰ TypeScript with Babel
  • πŸ‘‰ Watch Mode and Development Workflow
  • πŸ‘‰ Production Build Optimization

Configuring Webpack for TypeScript Projects πŸŽ‰

Setting up Webpack for your TypeScript projects can be a breeze! Let’s break it down step by step.

Getting Started with Webpack πŸš€

Webpack is the industry standard for complex applications requiring advanced features like code splitting and lazy loading. It powers production apps at companies like Airbnb, Pinterest, and Slack.

  1. Install Dependencies: You’ll need webpack, webpack-cli, ts-loader or babel-loader, and typescript. Use:
    1
    
    npm install --save-dev webpack webpack-cli ts-loader typescript
    
  2. Create webpack.config.js: Here’s a basic setup for a TypeScript + React project:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    const path = require('path');
    
    module.exports = {
        entry: './src/index.tsx',
        output: {
            filename: 'bundle.js',
            path: path.resolve(__dirname, 'dist'),
        },
        resolve: {
            extensions: ['.tsx', '.ts', '.js'],
        },
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    use: 'ts-loader',
                    exclude: /node_modules/,
                },
            ],
        },
        devtool: 'source-map', // For debugging
        optimization: {
            usedExports: true, // Enables tree shaking
        },
    };
    

Understanding Key Concepts 🧠

  • Source Maps: Helps you debug your TypeScript code easily by mapping compiled code back to the original source.
  • Tree Shaking: Removes unused code, making your bundle smaller. Ensure you use ES6 modules for this to work effectively.
  • Optimization Strategies: Use optimization settings in Webpack to improve performance, like enabling usedExports.

For more detailed info, check out the Webpack Documentation.

Real-World Example: Multi-Page Application Setup 🎯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// webpack.config.ts - Production-ready configuration for SaaS dashboard
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';

const config: webpack.Configuration = {
  mode: 'production',
  entry: {
    main: './src/index.tsx',
    admin: './src/admin.tsx',
    vendor: ['react', 'react-dom', 'axios']
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true // Clean dist folder before build
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      chunks: ['main', 'vendor']
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],
  devtool: 'source-map'
};

export default config;

Using Vite with TypeScript for Fast Development πŸš€

Why Choose Vite? πŸ€”

Vite is a modern build tool that offers native TypeScript support. It’s faster than Webpack because it uses native ES modules and only bundles code when needed. Companies like Shopify and Storybook use Vite for lightning-fast development experiences.

Setting Up Vite with TypeScript πŸ› οΈ

  1. Install Vite:
    1
    
    npm create vite@latest my-app --template vue-ts
    
  2. Configure vite.config.ts:
    • This file is where you set up plugins and other options.
    • Example:
      1
      2
      3
      4
      5
      6
      
      import { defineConfig } from 'vite';
      import vue from '@vitejs/plugin-vue';
      
      export default defineConfig({
        plugins: [vue()],
      });
      

Hot Module Replacement (HMR) πŸ”„

  • Vite supports HMR out of the box, allowing you to see changes instantly without refreshing the page.

Build Optimizations βš™οΈ

  • Vite optimizes your code for production, ensuring faster load times and smaller bundle sizes.

Advantages Over Webpack 🌟

  • Faster Development: Instant server start and updates.
  • Simpler Configuration: Less boilerplate code.
  • Better TypeScript Integration: No additional setup needed.

For more details, check out the Vite Documentation.

Real-World Example: Vue 3 + TypeScript Project 🎯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// vite.config.ts - Enterprise Vue.js application with TypeScript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    target: 'esnext',
    minify: 'terser',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-library': ['element-plus']
        }
      }
    },
    chunkSizeWarningLimit: 1000
  },
  optimizeDeps: {
    include: ['vue', 'axios']
  }
});
graph TD
    A["πŸš€ Vite Build Tool"]:::style1 --> B["πŸ“˜ TypeScript Support"]:::style2
    A --> C["⚑ Lightning Fast Dev"]:::style3
    A --> D["🎯 Build Optimizations"]:::style4
    B --> E["πŸ”₯ HMR"]:::style5
    B --> F["βš™οΈ Zero Config"]:::style2

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Introduction to esbuild: The Fastest TypeScript Bundler πŸš€

Are you looking for a super-fast way to bundle your TypeScript projects? Look no further than esbuild! This amazing tool is designed to make your development process quicker and smoother. Written in Go, esbuild is 10-100x faster than traditional JavaScript-based bundlers.

Why Choose esbuild? πŸ€”

  • Native TypeScript Support: esbuild can handle TypeScript files directly with its transpile-only feature. This means you can skip the type-checking step and focus on building your app quickly.

  • Configuring Build Scripts: Setting up esbuild is easy! You can create a simple build script using JavaScript or TypeScript. Here’s a basic example:

1
2
3
4
5
6
7
8
// build.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/bundle.js',
}).catch(() => process.exit(1));

Limitations ⚠️

  • No Type Checking: While esbuild is fast, it does not perform type checking. To ensure your code is error-free, you can combine it with tsc (TypeScript Compiler) for type checks.

Combining esbuild with tsc πŸ”—

You can run tsc before or after your esbuild process to catch any type errors. Here’s how you can do it:

1
tsc && node build.js

For more information, check out the esbuild documentation.

Real-World Example: Node.js Backend API 🎯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// build.ts - Production build script for Express API
import * as esbuild from 'esbuild';

async function build() {
  try {
    // Build for production
    await esbuild.build({
      entryPoints: ['src/server.ts'],
      bundle: true,
      platform: 'node',
      target: 'node18',
      outfile: 'dist/server.js',
      minify: true,
      sourcemap: true,
      external: [
        'express',
        'pg', // PostgreSQL client
        'redis',
        'dotenv'
      ],
      define: {
        'process.env.NODE_ENV': '"production"'
      },
      metafile: true,
      logLevel: 'info'
    });

    console.log('βœ… Build completed successfully!');
  } catch (error) {
    console.error('❌ Build failed:', error);
    process.exit(1);
  }
}

build();

// package.json scripts:
// "build": "tsc --noEmit && node build.ts"
// "dev": "tsx watch src/server.ts"

Building TypeScript Libraries with Rollup πŸŽ‰

Creating a TypeScript library can be fun and rewarding! Using Rollup with @rollup/plugin-typescript makes it easy to bundle your code efficiently. Popular libraries like Redux, Vue, and React Router are all built with Rollup.

Why Use Rollup? πŸ€”

Rollup is a module bundler that helps you:

  • Generate multiple formats: You can create ESM, CJS, and UMD builds.
  • Tree shaking: This means removing unused code, making your library smaller and faster.
  • Optimize your library: Rollup helps in producing efficient bundles.

Setting Up Rollup with TypeScript πŸ“¦

Here’s a simple rollup.config.ts example for your npm package:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import typescript from '@rollup/plugin-typescript';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/my-library.esm.js',
      format: 'esm',
    },
    {
      file: 'dist/my-library.cjs.js',
      format: 'cjs',
    },
    {
      file: 'dist/my-library.umd.js',
      format: 'umd',
      name: 'MyLibrary',
    },
  ],
  plugins: [typescript()],
};

Generating Declaration Files πŸ“œ

To generate TypeScript declaration files, add this to your tsconfig.json:

1
2
3
4
5
6
{
  "compilerOptions": {
    "declaration": true,
    "outDir": "./dist"
  }
}

Resources for More Info πŸ“š

Real-World Example: NPM Library Package 🎯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// rollup.config.ts - Complete configuration for publishing TypeScript library
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import dts from 'rollup-plugin-dts';

const production = !process.env.ROLLUP_WATCH;

export default [
  // JavaScript/TypeScript build
  {
    input: 'src/index.ts',
    output: [
      {
        file: 'dist/index.esm.js',
        format: 'esm',
        sourcemap: true
      },
      {
        file: 'dist/index.cjs.js',
        format: 'cjs',
        sourcemap: true,
        exports: 'named'
      },
      {
        file: 'dist/index.umd.js',
        format: 'umd',
        name: 'MyAwesomeLibrary',
        sourcemap: true,
        globals: {
          'axios': 'axios'
        }
      }
    ],
    plugins: [
      resolve(), // Resolve node_modules
      commonjs(), // Convert CommonJS to ES modules
      typescript({
        tsconfig: './tsconfig.json',
        declaration: false // We'll generate .d.ts separately
      }),
      production && terser() // Minify in production
    ],
    external: ['axios'] // Don't bundle peer dependencies
  },
  // TypeScript declaration files (.d.ts)
  {
    input: 'src/index.ts',
    output: [{ file: 'dist/index.d.ts', format: 'esm' }],
    plugins: [dts()]
  }
];

// package.json:
// {
//   "main": "dist/index.cjs.js",
//   "module": "dist/index.esm.js",
//   "types": "dist/index.d.ts",
//   "files": ["dist"]
// }

Using Babel to Transpile TypeScript πŸš€

Transpiling TypeScript with Babel can be a great choice for developers looking for speed and flexibility. Teams already using Babel for JavaScript can seamlessly add TypeScript support without changing their entire build pipeline.

What is Babel?

Babel is a popular tool that converts modern JavaScript (and TypeScript) into a version that can run in older environments. With the @babel/preset-typescript, you can easily transpile TypeScript files.

Key Differences from tsc

  • Faster Compilation: Babel focuses on transforming code, making it quicker than TypeScript’s compiler (tsc), which also checks types.
  • No Type Checking: Babel does not perform type checking. This means it’s faster, but you might miss type errors.

Combining with tsc for Type Checking

You can use Babel for transpilation and tsc for type checking. This way, you get the best of both worlds!

1
tsc --noEmit && babel src --out-dir dist

Integration with Existing Babel Pipelines

If you already use Babel for JavaScript, adding TypeScript is seamless. Just install the preset:

1
npm install --save-dev @babel/preset-typescript

Then, add it to your Babel configuration.

Trade-offs to Consider

  • Speed vs. Safety: Choose Babel for speed, but remember to use tsc for type safety.
  • Setup Complexity: Integrating Babel with TypeScript can add complexity to your build process.

For more detailed information, check out the Babel Documentation.

flowchart TD
    A["🎯 Start Build Process"]:::style1 --> B{"πŸ’‘ Use Babel?"}:::style3
    B -- "Yes ⚑" --> C["πŸ”„ Transpile with Babel"]:::style2
    B -- "No πŸ›‘οΈ" --> D["πŸ” Use tsc Type Check"]:::style4
    C --> E["βœ… Run Application"]:::style5
    D --> E

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Understanding TypeScript Watch Mode and Development Tools

What is TypeScript Watch Mode? πŸ‘€

TypeScript’s watch mode (tsc --watch) is a handy feature that automatically compiles your code whenever you make changes. This means you don’t have to run the compiler manually every time! Modern IDEs like VS Code integrate watch mode seamlessly for real-time error feedback.

Incremental Compilation πŸš€

With incremental compilation, TypeScript only recompiles the files that have changed. This speeds up the process, making your development experience smoother and faster!

Using Tools for Node.js Development πŸ› οΈ

Tools like nodemon and ts-node-dev help you run your TypeScript applications efficiently:

  • Nodemon: Automatically restarts your app when files change.
  • ts-node-dev: Combines TypeScript compilation and nodemon features for a seamless experience.

Hot Module Replacement (HMR) πŸ”₯

In web apps, HMR allows you to update modules in real-time without a full page reload. This keeps your app state intact and speeds up development!

Optimizing Build Times ⏱️

To optimize build times:

  • Use watch mode for automatic compilation.
  • Leverage incremental compilation to avoid recompiling everything.
  • Consider using HMR for instant updates in web apps.

For more information, check out the TypeScript Documentation.

graph TD
    A["πŸš€ Start Development"]:::style1 --> B{"πŸ“ Make Changes?"}:::style3
    B -- "Yes πŸ”„" --> C["βš™οΈ Compile Code"]:::style2
    C --> D["πŸƒ Run Application"]:::style4
    D --> A
    B -- "No ⏸️" --> A

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Optimizing TypeScript Builds for Production

Optimizing your TypeScript builds is essential for creating efficient and fast applications. Production builds can reduce bundle sizes by 60-80% through proper optimization techniques.

Key Optimization Techniques

1. Minification

  • What: Reduces file size by removing whitespace and comments.
  • How: Use tools like Terser or UglifyJS.

2. Tree Shaking

  • What: Removes unused code from your bundles.
  • How: Ensure your modules are ES6 and use tools like Webpack.

3. Code Splitting

  • What: Breaks your code into smaller chunks.
  • How: Use dynamic imports with Webpack.

4. Removing Dead Code

  • What: Eliminates code that is never used.
  • How: Use tools like eslint to identify and remove it.

5. Generating Source Maps

  • What: Helps in debugging by mapping minified code back to original.
  • How: Enable source maps in your build tool configuration.

Best Practices

  • Target ES Version: Choose a version that balances compatibility and performance (e.g., ES2015).
  • Polyfills: Use polyfills for features not supported in older browsers (e.g., core-js).
  • Bundle Analysis: Use tools like Webpack Bundle Analyzer to visualize your bundle size.

Resources

By following these tips, you can ensure your TypeScript applications are optimized for production!


🎯 Hands-On Assignment: Build a Multi-Tool TypeScript Project πŸš€

πŸ“ Your Mission

Create a production-ready TypeScript project that demonstrates mastery of multiple build tools. You'll build a simple task management library that can be bundled for different environments (Node.js, browser, npm package) using Webpack, Vite, esbuild, and Rollup.

🎯 Requirements

  1. Create a TypeScript library with at least 3 modules:
    • TaskManager.ts - Core task management class
    • Storage.ts - LocalStorage/File system abstraction
    • Validator.ts - Input validation utilities
  2. Configure Webpack for browser bundle with:
    • TypeScript compilation using ts-loader
    • Code splitting for lazy-loaded modules
    • Source maps for debugging
    • Minification for production
  3. Setup Vite for development with:
    • Hot Module Replacement (HMR)
    • Demo HTML page using the library
    • Proxy configuration for API calls
  4. Add esbuild script for fast Node.js builds:
    • Bundle for node18 target
    • External dependencies marked properly
    • Combined with tsc --noEmit for type checking
  5. Configure Rollup for npm package publishing:
    • Generate ESM, CJS, and UMD formats
    • Create TypeScript declaration files (.d.ts)
    • Proper package.json configuration

πŸ’‘ Implementation Hints

  1. Use tsconfig.json with "declaration": true for generating type definitions
  2. For Webpack, enable tree shaking with optimization.usedExports: true
  3. In Vite config, use build.rollupOptions.output.manualChunks for code splitting
  4. Use esbuild.build({ platform: 'node', bundle: true }) for Node.js
  5. In Rollup, add rollup-plugin-dts for combining declaration files
  6. Test each build output: run browser bundle in HTML, Node.js bundle with node, npm package in separate project

πŸš€ Example Structure

// src/TaskManager.ts
export interface Task {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

export class TaskManager {
  private tasks: Map<string, Task> = new Map();

  addTask(title: string): Task {
    const task: Task = {
      id: crypto.randomUUID(),
      title,
      completed: false,
      createdAt: new Date()
    };
    this.tasks.set(task.id, task);
    return task;
  }

  completeTask(id: string): boolean {
    const task = this.tasks.get(id);
    if (task) {
      task.completed = true;
      return true;
    }
    return false;
  }

  getAllTasks(): Task[] {
    return Array.from(this.tasks.values());
  }
}

// Build outputs:
// - dist/browser/bundle.js (Webpack - for 
This post is licensed under CC BY 4.0 by the author.