Lesson 30-Rollup and Vite

Rollup Build Principles Underlying Vite

Rollup is the core engine used by Vite during the build phase, responsible for bundling project modules into a format that browsers can understand. Rollup is designed to provide efficient bundling solutions tailored to the modular characteristics of modern JavaScript, particularly ES Modules.

Overview of Rollup Build Process

Rollup’s build process can be divided into the following stages:

  • Parsing: Reads source code and converts it into an Abstract Syntax Tree (AST).
  • Transforming: Traverses the AST and applies transformation rules provided by plugins.
  • Resolving Dependencies: Identifies the dependency relationships for each module.
  • Building the Module Graph: Creates a graph representing the dependency relationships among all modules in the project.
  • Generating Code: Produces the final output code from the module graph.
  • Outputting: Writes the generated code to the filesystem.

Rollup’s Plugin Mechanism

Rollup’s power lies in its plugin mechanism, which allows plugins to extend functionality and handle different file types or perform specific transformations. Plugins can influence the build process in the following ways:

  • transform: Modifies module source code.
  • load: Loads content for non-JavaScript files.
  • resolveId: Resolves module IDs to determine their actual locations.
  • generateBundle: Modifies or adds new modules before generating the output.
  • writeBundle: Makes final modifications before writing to the filesystem.

Deep Dive into Rollup’s Build Principles

Let’s explore Rollup’s build process with a simple example. Assume the following project structure:

src/
├── main.js
└── lib/
    └── index.js

With the following content:

// src/main.js
import { sayHello } from './lib/index';
sayHello();
// src/lib/index.js
export function sayHello() {
  console.log('Hello, Rollup!');
}

Build Process:

  1. Parsing and Transforming: Rollup reads main.js and lib/index.js, parses them into ASTs, and applies any plugin transformation rules.
  2. Resolving Dependencies: Rollup identifies that main.js depends on lib/index.js.
  3. Building the Module Graph: Rollup creates a data structure representing all modules and their dependencies.
  4. Generating Code: Rollup uses the module graph to produce the final output code, typically one or more JavaScript files containing all necessary module code and import/export statements.
  5. Outputting: The generated code is written to the specified output directory.

Building with Rollup

In practice, you may not use Rollup directly but rather through a tool like Vite. However, in some cases, such as dynamic builds or plugin testing, you might need to use Rollup directly.

Here’s a basic example of building with Rollup:

const rollup = require('rollup');
const babel = require('rollup-plugin-babel');

rollup.rollup({
  input: 'src/main.js',
  plugins: [
    babel({
      exclude: 'node_modules/**' // Transpile only source code, not node_modules
    })
  ]
}).then(bundle => {
  bundle.write({
    format: 'iife', // Immediately Invoked Function Expression
    file: 'dist/bundle.js',
    name: 'app',
    sourcemap: true
  });
});

In this example, the Babel plugin is used to transpile source code for compatibility with older browsers.

Leveraging Rollup’s Advanced Features

Rollup is a JavaScript module bundler designed for efficient handling of modern modular code. Beyond basic bundling, Rollup offers advanced features that allow developers to finely control the build process, optimize build speed, and manage complex project structures.

Code Splitting

Code splitting is an optimization strategy that divides code into smaller chunks, loading only the parts needed at runtime. This significantly reduces initial load times and improves performance. Rollup supports code splitting based on dynamic import().

// app.js
import('./chunk.js').then(chunk => {
  chunk.default(); // Dynamically load chunk.js
});

Build Configuration:

const rollup = require('rollup');
const commonjs = require('@rollup/plugin-commonjs');
const nodeResolve = require('@rollup/plugin-node-resolve');

rollup.rollup({
  input: 'app.js',
  plugins: [
    commonjs(),
    nodeResolve()
  ]
}).then(bundle => {
  bundle.write({
    format: 'iife', // Immediately Invoked Function Expression
    file: 'bundle.js',
    name: 'app',
    sourcemap: true
  });
});

Plugin Development

Rollup’s plugin mechanism is highly flexible, allowing developers to customize the build process, handle non-JavaScript files, and perform various transformations. Developing a plugin involves defining hooks like transform, load, and resolveId.

Example Plugin:

// my-plugin.js
module.exports = function myPlugin(options) {
  return {
    name: 'my-plugin',
    transform(code, id) {
      if (id.endsWith('.js')) {
        return code.replace(/console.log/g, 'console.info');
      }
    }
  };
};

Using the Plugin:

const rollup = require('rollup');
const myPlugin = require('./my-plugin');

rollup.rollup({
  input: 'app.js',
  plugins: [
    myPlugin()
  ]
}).then(bundle => {
  // ...
});

Dynamic Imports

Dynamic imports enable asynchronous module loading at runtime, ideal for on-demand and lazy loading. Rollup handles dynamic imports and automatically performs code splitting.

// app.js
document.getElementById('button').addEventListener('click', () => {
  import('./chunk.js').then(chunk => {
    chunk.default();
  });
});

Conditional Compilation

Rollup supports conditional compilation based on environment variables, allowing developers to switch code paths during the build process based on the environment.

if (process.env.NODE_ENV === 'production') {
  console.log('Production mode');
} else {
  console.log('Development mode');
}

Build Configuration:

const rollup = require('rollup');
const replace = require('rollup-plugin-replace');

rollup.rollup({
  input: 'app.js',
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ]
}).then(bundle => {
  // ...
});

Tree Shaking

Tree shaking is a key Rollup feature that removes unused code, retaining only what is actually needed to reduce bundle size.

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// app.js
import { add } from './math';

Build Result: After building, the subtract function is removed since it is not used in app.js.

Custom Output Formats

Rollup supports various output formats, including IIFE, CJS, ESM, and AMD. Developers can choose the appropriate format based on project needs.

Example Configuration:

const rollup = require('rollup');

rollup.rollup({
  input: 'app.js',
}).then(bundle => {
  bundle.write({
    format: 'esm', // Output as ESM format
    file: 'bundle.js',
    sourcemap: true
  });
});

Custom Rollup Configuration and Vite Integration

Vite uses ES Modules for development but relies on Rollup for module bundling during the build phase. While Vite provides a default build configuration, you can customize Rollup configurations to meet more complex build requirements.

Understanding Vite’s Rollup Configuration

Vite allows customization of Rollup’s build configuration in the vite.config.js file. Vite automatically passes these configurations to Rollup during the build process.

Customizing Rollup Configuration

In vite.config.js, use the build.rollupOptions property to customize Rollup configurations, such as adding plugins, modifying input/output options, or altering Rollup’s default behavior.

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: 'src/main.js', // Specify entry file
      output: {
        dir: 'dist', // Output directory
        format: 'es', // Output format
        chunkFileNames: 'chunks/[name]-[hash].js', // Naming rule for split chunks
        entryFileNames: '[name]-[hash].js', // Naming rule for entry files
        assetFileNames: '[name]-[hash].[ext]' // Naming rule for non-code assets
      },
      plugins: [
        // Add custom plugin
        {
          name: 'my-plugin',
          // Plugin logic...
        }
      ],
      external: ['lodash'], // External dependencies not bundled
      treeshake: {
        moduleSideEffects: 'no-external' // More aggressive tree shaking
      }
    }
  }
});

Using Custom Rollup Plugins

Vite supports custom Rollup plugins during the build phase. Add these plugins to build.rollupOptions.plugins.

// vite.config.js
import { defineConfig } from 'vite';
import myCustomPlugin from './my-custom-plugin'; // Import custom plugin

export default defineConfig({
  build: {
    rollupOptions: {
      plugins: [
        myCustomPlugin()
      ]
    }
  }
});

Configuring Rollup Plugins

Vite includes default Rollup plugins like @rollup/plugin-node-resolve and @rollup/plugin-commonjs. Configure these or add custom plugins via build.rollupOptions.plugins.

// vite.config.js
import { defineConfig } from 'vite';
import commonjs from '@rollup/plugin-commonjs';

export default defineConfig({
  build: {
    rollupOptions: {
      plugins: [
        commonjs({
          include: /node_modules/,
          extensions: ['.js', '.jsx', '.ts', '.tsx']
        })
      ]
    }
  }
});

Adjusting Rollup Build Behavior

Beyond plugins, use build.rollupOptions to adjust Rollup’s build behavior, such as modifying input/output options, externalizing dependencies, or tweaking tree shaking strategies.

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'src/main.js', // Multiple entry files
        other: 'src/other.js'
      },
      output: {
        dir: 'dist',
        format: 'es',
        entryFileNames: '[name].js',
        chunkFileNames: '[name]-[hash].js',
        assetFileNames: '[name]-[hash].[ext]'
      },
      external: ['react', 'react-dom'], // Externalize React and ReactDOM
      treeshake: {
        propertyReadSideEffects: false // More aggressive tree shaking
      }
    }
  }
});

Testing and Debugging Custom Configurations

When customizing configurations, test the build results using the vite build command to ensure everything works as expected. If issues arise, consult Vite and Rollup documentation or use Vite’s --config option to specify a different configuration file for debugging.

Share your love