# Rollup Plugins: Extend Rollup Bundler

# Rollup Plugins: Extend Rollup Bundler

## Table of Contents

1. [Introduction](#introduction)
2. [Understanding Rollup Hooks](#understanding-rollup-hooks)
3. [Plugin Structure](#plugin-structure)
4. [Build Hooks](#build-hooks)
5. [Output Generation Hooks](#output-generation-hooks)
6. [Creating Custom Plugins](#creating-custom-plugins)
7. [Real-World Examples](#real-world-examples)
8. [FAQ](#faq)

## Introduction

Rollup plugins are the primary way to extend Rollup's functionality. They use a **hook-based architecture** that allows you to tap into different stages of the bundling process. Plugins can transform code, resolve imports, load files, and modify the final output.

**Key Benefits:**
- Modular and composable architecture
- Access to the entire build pipeline
- Rich ecosystem of existing plugins
- Type-safe with TypeScript support

## Understanding Rollup Hooks

Rollup hooks are functions that execute at specific points during the build process. They fall into two main categories:

### Hook Types

1. **Build Hooks**: Run during the build phase (parsing, transforming)
2. **Output Generation Hooks**: Run during output generation (rendering, writing)

### Hook Execution Order

```
options → buildStart → resolveId → load → transform → 
moduleParsed → buildEnd → outputOptions → renderStart → 
banner/footer/intro/outro → renderChunk → generateBundle → 
writeBundle → closeBundle
```

## Plugin Structure

### Basic Plugin Anatomy

```javascript
export default function myPlugin(options = {}) {
  return {
    name: 'my-plugin', // Required: plugin name
    
    // Hook functions
    buildStart(options) {
      // Initialization logic
    },
    
    resolveId(source, importer) {
      // Custom module resolution
    },
    
    load(id) {
      // Custom module loading
    },
    
    transform(code, id) {
      // Code transformation
    }
  };
}
```

### Plugin Options Pattern

```javascript
export default function myPlugin(userOptions = {}) {
  const defaultOptions = {
    include: '**/*.js',
    exclude: 'node_modules/**',
    verbose: false
  };
  
  const options = { ...defaultOptions, ...userOptions };
  
  return {
    name: 'my-plugin',
    // Use options in hooks
  };
}
```

## Build Hooks

### 1. `options` Hook

Modify or read Rollup input options.

```javascript
options(inputOptions) {
  return {
    ...inputOptions,
    // Modify options
    treeshake: true
  };
}
```

### 2. `buildStart` Hook

Called when the build starts. Good for initialization.

```javascript
buildStart(options) {
  console.log('Build starting with options:', options);
  this.cache = new Map(); // Initialize plugin state
}
```

### 3. `resolveId` Hook

Custom module resolution logic.

```javascript
resolveId(source, importer, options) {
  // Handle virtual modules
  if (source === 'virtual-module') {
    return source; // Signals this plugin will load it
  }
  
  // Handle custom protocols
  if (source.startsWith('custom:')) {
    return {
      id: source.slice(7),
      external: false
    };
  }
  
  return null; // Let other plugins/default resolver handle it
}
```

### 4. `load` Hook

Load module content from custom sources.

```javascript
load(id) {
  // Load virtual modules
  if (id === 'virtual-module') {
    return 'export default "Virtual content"';
  }
  
  // Load from custom sources
  if (id.startsWith('db:')) {
    return fetchFromDatabase(id.slice(3));
  }
  
  return null; // Use default loader
}
```

### 5. `transform` Hook

Transform module code (most commonly used).

```javascript
transform(code, id) {
  // Skip non-JS files
  if (!id.endsWith('.js')) return null;
  
  // Simple transformation
  const transformedCode = code.replace(/OLD_API/g, 'NEW_API');
  
  return {
    code: transformedCode,
    map: null // or generate source map
  };
}
```

**With Source Maps:**

```javascript
import MagicString from 'magic-string';

transform(code, id) {
  const magicString = new MagicString(code);
  
  // Make changes
  magicString.replace('foo', 'bar');
  
  return {
    code: magicString.toString(),
    map: magicString.generateMap({ hires: true })
  };
}
```

### 6. `moduleParsed` Hook

Called after a module has been parsed.

```javascript
moduleParsed(moduleInfo) {
  console.log(`Parsed: ${moduleInfo.id}`);
  console.log(`Imports: ${moduleInfo.importedIds.join(', ')}`);
  console.log(`Exports: ${moduleInfo.exports.join(', ')}`);
}
```

## Output Generation Hooks

### 1. `renderChunk` Hook

Transform individual chunks before writing.

```javascript
renderChunk(code, chunk, options) {
  // Add banner to each chunk
  const banner = `/* Chunk: ${chunk.fileName} */\n`;
  
  return {
    code: banner + code,
    map: null
  };
}
```

### 2. `generateBundle` Hook

Modify or add files to the bundle before writing.

```javascript
generateBundle(options, bundle) {
  // Add a custom file
  this.emitFile({
    type: 'asset',
    fileName: 'manifest.json',
    source: JSON.stringify({
      files: Object.keys(bundle)
    })
  });
  
  // Modify existing files
  for (const fileName in bundle) {
    const chunk = bundle[fileName];
    if (chunk.type === 'chunk') {
      // Modify chunk code
      chunk.code = `/* Modified */\n${chunk.code}`;
    }
  }
}
```

### 3. `writeBundle` Hook

Called after files have been written to disk.

```javascript
async writeBundle(options, bundle) {
  console.log('Bundle written to:', options.dir);
  
  // Post-processing tasks
  await compressFiles(options.dir);
  await uploadToServer(bundle);
}
```

## Creating Custom Plugins

### Example 1: Environment Variable Injection

```javascript
import { createFilter } from '@rollup/pluginutils';

export default function envPlugin(options = {}) {
  const filter = createFilter(
    options.include || '**/*.js',
    options.exclude || 'node_modules/**'
  );
  
  const env = options.env || process.env;
  
  return {
    name: 'env-injection',
    
    transform(code, id) {
      if (!filter(id)) return null;
      
      let transformed = code;
      
      // Replace process.env.VAR with actual values
      Object.keys(env).forEach(key => {
        const regex = new RegExp(
          `process\\.env\\.${key}\\b`,
          'g'
        );
        transformed = transformed.replace(
          regex,
          JSON.stringify(env[key])
        );
      });
      
      if (transformed !== code) {
        return { code: transformed, map: null };
      }
      
      return null;
    }
  };
}
```

**Usage:**

```javascript
// rollup.config.js
import envPlugin from './plugins/env-plugin.js';

export default {
  input: 'src/index.js',
  output: { file: 'dist/bundle.js', format: 'es' },
  plugins: [
    envPlugin({
      env: {
        API_URL: 'https://api.example.com',
        DEBUG: 'true'
      }
    })
  ]
};
```

### Example 2: File Size Reporter

```javascript
export default function sizeReporter(options = {}) {
  const threshold = options.threshold || 100 * 1024; // 100KB
  
  return {
    name: 'size-reporter',
    
    generateBundle(outputOptions, bundle) {
      console.log('\n📦 Bundle Size Report:\n');
      
      let totalSize = 0;
      
      for (const [fileName, file] of Object.entries(bundle)) {
        const size = file.type === 'chunk' 
          ? Buffer.byteLength(file.code, 'utf8')
          : file.source.length;
        
        totalSize += size;
        
        const sizeKB = (size / 1024).toFixed(2);
        const warning = size > threshold ? ' ⚠️' : '';
        
        console.log(`  ${fileName}: ${sizeKB} KB${warning}`);
      }
      
      console.log(`\n  Total: ${(totalSize / 1024).toFixed(2)} KB\n`);
    }
  };
}
```

### Example 3: Virtual Module Plugin

```javascript
export default function virtualModule(modules = {}) {
  const PREFIX = '\0virtual:';
  
  return {
    name: 'virtual-module',
    
    resolveId(id) {
      if (id in modules) {
        return PREFIX + id;
      }
      return null;
    },
    
    load(id) {
      if (id.startsWith(PREFIX)) {
        const moduleName = id.slice(PREFIX.length);
        return modules[moduleName];
      }
      return null;
    }
  };
}
```

**Usage:**

```javascript
// rollup.config.js
import virtualModule from './plugins/virtual-module.js';

export default {
  plugins: [
    virtualModule({
      'config': 'export default { version: "1.0.0" };',
      'constants': 'export const PI = 3.14159;'
    })
  ]
};

// In your code:
// import config from 'config';
// import { PI } from 'constants';
```

### Example 4: Import Analyzer

```javascript
export default function importAnalyzer() {
  const importGraph = new Map();
  
  return {
    name: 'import-analyzer',
    
    moduleParsed(moduleInfo) {
      importGraph.set(moduleInfo.id, {
        imports: moduleInfo.importedIds,
        exports: moduleInfo.exports,
        dynamicImports: moduleInfo.dynamicallyImportedIds
      });
    },
    
    buildEnd() {
      // Find circular dependencies
      const circular = findCircularDeps(importGraph);
      
      if (circular.length > 0) {
        console.warn('⚠️  Circular dependencies detected:');
        circular.forEach(cycle => {
          console.warn(`  ${cycle.join(' → ')}`);
        });
      }
      
      // Find unused modules
      const entryPoints = new Set(['src/index.js']);
      const used = findReachable(importGraph, entryPoints);
      const unused = [...importGraph.keys()].filter(
        id => !used.has(id) && !id.includes('node_modules')
      );
      
      if (unused.length > 0) {
        console.warn('\n📦 Potentially unused modules:');
        unused.forEach(id => console.warn(`  ${id}`));
      }
    }
  };
}

function findCircularDeps(graph) {
  // Implementation of cycle detection
  const cycles = [];
  const visited = new Set();
  const stack = new Set();
  
  function dfs(node, path = []) {
    if (stack.has(node)) {
      const cycleStart = path.indexOf(node);
      cycles.push(path.slice(cycleStart).concat(node));
      return;
    }
    
    if (visited.has(node)) return;
    
    visited.add(node);
    stack.add(node);
    path.push(node);
    
    const nodeData = graph.get(node);
    if (nodeData) {
      nodeData.imports.forEach(dep => dfs(dep, [...path]));
    }
    
    stack.delete(node);
  }
  
  graph.forEach((_, node) => dfs(node));
  return cycles;
}

function findReachable(graph, entryPoints) {
  const reachable = new Set();
  
  function traverse(node) {
    if (reachable.has(node)) return;
    reachable.add(node);
    
    const nodeData = graph.get(node);
    if (nodeData) {
      nodeData.imports.forEach(traverse);
      nodeData.dynamicImports.forEach(traverse);
    }
  }
  
  entryPoints.forEach(traverse);
  return reachable;
}
```

## Real-World Examples

### Complete Plugin: CSS Modules

```javascript
import { createFilter } from '@rollup/pluginutils';
import postcss from 'postcss';
import postcssModules from 'postcss-modules';

export default function cssModules(options = {}) {
  const filter = createFilter(
    options.include || '**/*.module.css',
    options.exclude
  );
  
  const cssMap = new Map();
  
  return {
    name: 'css-modules',
    
    async transform(code, id) {
      if (!filter(id)) return null;
      
      let cssExports = {};
      
      const result = await postcss([
        postcssModules({
          getJSON(filename, json) {
            cssExports = json;
          }
        })
      ]).process(code, { from: id });
      
      // Store CSS for later
      cssMap.set(id, result.css);
      
      // Return JS module that exports class names
      return {
        code: `export default ${JSON.stringify(cssExports)};`,
        map: { mappings: '' }
      };
    },
    
    generateBundle() {
      // Combine all CSS
      const allCSS = Array.from(cssMap.values()).join('\n');
      
      // Emit as asset
      this.emitFile({
        type: 'asset',
        fileName: 'styles.css',
        source: allCSS
      });
    }
  };
}
```

### Plugin with Caching

```javascript
import { createHash } from 'crypto';

export default function cachedTransform() {
  let cache = new Map();
  
  return {
    name: 'cached-transform',
    
    buildStart() {
      // Load cache from previous build
      if (this.meta.watchMode) {
        cache = this.meta.cache || new Map();
      }
    },
    
    transform(code, id) {
      const hash = createHash('md5').update(code).digest('hex');
      
      // Check cache
      if (cache.has(id)) {
        const cached = cache.get(id);
        if (cached.hash === hash) {
          return cached.result;
        }
      }
      
      // Expensive transformation
      const result = expensiveTransform(code);
      
      // Store in cache
      cache.set(id, { hash, result });
      
      return result;
    },
    
    buildEnd() {
      // Persist cache for watch mode
      if (this.meta.watchMode) {
        this.meta.cache = cache;
      }
    }
  };
}
```

## FAQ

### Q: What's the difference between `transform` and `renderChunk`?

**A:** `transform` operates on individual modules during the build phase, while `renderChunk` operates on the final bundled chunks during output generation. Use `transform` for module-level transformations and `renderChunk` for chunk-level modifications.

### Q: How do I handle source maps in plugins?

**A:** Use the `magic-string` library for simple transformations, or generate source maps manually. Always return both `code` and `map` properties:

```javascript
import MagicString from 'magic-string';

transform(code, id) {
  const s = new MagicString(code);
  s.replace('foo', 'bar');
  
  return {
    code: s.toString(),
    map: s.generateMap({ hires: true })
  };
}
```

### Q: When should I return `null` from a hook?

**A:** Return `null` when your plugin doesn't need to handle that particular module/situation. This allows other plugins or Rollup's default behavior to take over.

### Q: How do I emit additional files?

**A:** Use `this.emitFile()` in hooks like `generateBundle`:

```javascript
generateBundle() {
  this.emitFile({
    type: 'asset',
    fileName: 'extra-file.txt',
    source: 'content'
  });
}
```

### Q: Can plugins have side effects?

**A:** Yes, but be careful. Use `moduleSideEffects` to control tree-shaking:

```javascript
resolveId(id) {
  if (id === 'my-module') {
    return {
      id,
      moduleSideEffects: true // Prevent tree-shaking
    };
  }
}
```

### Q: How do I debug plugins?

**A:** Use console logging, or leverage Rollup's `--verbose` flag. You can also use the `buildStart` hook to log configuration:

```javascript
buildStart(options) {
  if (this.meta.watchMode) {
    console.log('Running in watch mode');
  }
  console.log('Input options:', options);
}
```

### Q: What's the `\0` prefix convention?

**A:** The `\0` prefix is used for virtual module IDs to prevent conflicts with real file paths. It's a convention, not a requirement:

```javascript
resolveId(id) {
  if (id === 'virtual') {
    return '\0virtual'; // Virtual module
  }
}
```

### Q: How do I make plugins work with TypeScript?

**A:** Add TypeScript definitions:

```typescript
import { Plugin } from 'rollup';

interface MyPluginOptions {
  include?: string;
  exclude?: string;
}

export default function myPlugin(
  options: MyPluginOptions = {}
): Plugin {
  return {
    name: 'my-plugin',
    // ... hooks
  };
}
```

### Q: Can I use async functions in hooks?

**A:** Yes! Most hooks support async/await:

```javascript
async load(id) {
  if (id.startsWith('http://')) {
    const response = await fetch(id);
    return await response.text();
  }
}
```

### Q: How do I share state between hooks?

**A:** Use closure variables or `this` context:

```javascript
export default function myPlugin() {
  const state = new Map(); // Shared across hooks
  
  return {
    name: 'my-plugin',
    
    buildStart() {
      state.set('startTime', Date.now());
    },
    
    buildEnd() {
      const duration = Date.now() - state.get('startTime');
      console.log(`Build took ${duration}ms`);
    }
  };
}
