Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.webpack.js.org/llms.txt

Use this file to discover all available pages before exploring further.

What is Pitching?

The pitching phase is an advanced loader feature that allows loaders to execute before the normal loader chain. Loaders run in two phases:
  1. Pitching phase: Left to right (or top to bottom)
  2. Normal phase: Right to left (or bottom to top)

Execution Flow

Given this loader configuration:
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['loader1', 'loader2', 'loader3']
      }
    ]
  }
};
The execution order is:
┌─────────────────────────────────────────────────────┐
│                  Pitching Phase                     │
│  (left to right / top to bottom)                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  loader1.pitch → loader2.pitch → loader3.pitch     │
│                                              ↓      │
│                                         Read File   │
│                                              ↓      │
│  loader1 ← loader2 ← loader3 ← file content        │
│                                                     │
├─────────────────────────────────────────────────────┤
│                   Normal Phase                      │
│  (right to left / bottom to top)                    │
└─────────────────────────────────────────────────────┘

Defining a Pitch Function

A pitch function is defined as a property on the loader:
module.exports = function(source) {
  // Normal loader
  return transform(source);
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  // Pitch loader
  console.log('Pitching!');
};

Pitch Function Parameters

The pitch function receives three parameters:

Example: Inspecting Parameters

// loader2.js
module.exports = function(source) {
  console.log('Normal phase - data:', this.data);
  return source;
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  console.log('Pitching phase');
  console.log('Remaining:', remainingRequest);
  // loader3!/path/to/file.js
  
  console.log('Preceding:', precedingRequest);
  // loader1
  
  // Store data for normal phase
  data.value = 'shared data';
};

Short-Circuiting the Loader Chain

If a pitch function returns a value, it short-circuits the loader chain:
  1. Remaining pitches are skipped
  2. The resource file is not read
  3. Execution jumps back to preceding loaders

Example: Short-Circuiting

// style-loader.js (simplified)
module.exports.pitch = function(remainingRequest) {
  // Return early with inline require
  return (
    `var content = require(${JSON.stringify(remainingRequest)});` +
    `if (typeof content === 'string') content = [[module.id, content]];` +
    `var update = require('style-loader/addStyles')(content);` +
    `if (content.locals) module.exports = content.locals;`
  );
};
Execution flow when loader1 returns in pitch:
loader1.pitch (returns value) ───────┐


                             Skip loader2.pitch
                             Skip loader3.pitch
                             Skip reading file


                             (no preceding loaders)


                                  Webpack
Execution flow when loader2 returns in pitch:
loader1.pitch ────────────────────────┐


loader2.pitch (returns value) ────────┼──────┐
                                      │      │
                                      ↓      │
                             Skip loader3.pitch
                             Skip reading file


                                        loader1


                                         Webpack

Use Cases

1. Inline Requests

Process the remaining loaders inline instead of separately:
module.exports.pitch = function(remainingRequest) {
  return `
    import content from ${JSON.stringify('-!' + remainingRequest)};
    export default content;
  `;
};
The -! prefix disables all configured loaders.

2. Early Validation

Validate inputs before processing:
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  const options = this.getOptions();
  
  // Validate early
  if (!options.required) {
    throw new Error('Required option missing');
  }
  
  // Store for normal phase
  data.options = options;
};

module.exports = function(source) {
  const options = this.data.options;
  return transform(source, options);
};

3. Caching Information

Collect information during pitching for use in the normal phase:
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  // Cache expensive computations
  data.startTime = Date.now();
  data.resourcePath = this.resourcePath;
};

module.exports = function(source) {
  const duration = Date.now() - this.data.startTime;
  console.log(`Processed ${this.data.resourcePath} in ${duration}ms`);
  return source;
};

4. Conditional Loading

Decide whether to process based on file metadata:
module.exports.pitch = function(remainingRequest) {
  const callback = this.async();
  
  this.fs.stat(this.resourcePath, (err, stats) => {
    if (err) return callback(err);
    
    // Skip large files
    if (stats.size > 1024 * 1024) {
      return callback(
        null,
        `module.exports = 'File too large'`
      );
    }
    
    // Continue normal execution
    callback();
  });
};

Real-World Example: style-loader

The style-loader uses pitching to inject CSS into the DOM:
// style-loader (simplified)
module.exports = function() {};

module.exports.pitch = function(remainingRequest) {
  // This runs BEFORE css-loader
  // It returns code that will require() the result of css-loader
  return `
    var content = require(${JSON.stringify('!!' + remainingRequest)});
    var api = require('style-loader/runtime/injectStylesIntoStyleTag');
    var update = api(content);
    
    if (module.hot) {
      module.hot.accept(${JSON.stringify('!!' + remainingRequest)}, function() {
        var newContent = require(${JSON.stringify('!!' + remainingRequest)});
        update(newContent);
      });
      
      module.hot.dispose(function() {
        update();
      });
    }
    
    module.exports = content.locals || {};
  `;
};
Why use pitching?
  1. style-loader needs the output of css-loader, not the input
  2. By returning in the pitch phase, it can inline a require() call
  3. The require() goes through the remaining loaders (css-loader)
  4. The result is injected into the DOM

Data Sharing Between Phases

Use the data parameter to share information:
module.exports = function(source) {
  // Access data from pitch phase
  const { count, timestamp } = this.data;
  
  console.log(`Processing file #${count} at ${timestamp}`);
  return source;
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  // Store data for normal phase
  data.count = ++globalCount;
  data.timestamp = Date.now();
  data.remainingRequest = remainingRequest;
};
The data object is available as this.data in the normal loader function.

Async Pitching

Pitch functions can be asynchronous:
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  const callback = this.async();
  
  someAsyncOperation(this.resourcePath, (err, result) => {
    if (err) return callback(err);
    
    if (result.shouldSkip) {
      // Short-circuit
      return callback(null, result.code);
    }
    
    // Continue to next pitch/loader
    callback();
  });
};
Or with Promises:
module.exports.pitch = async function(remainingRequest) {
  const metadata = await fetchMetadata(this.resourcePath);
  
  if (metadata.shouldSkip) {
    // Short-circuit
    return generateCode(metadata);
  }
  
  // Continue to next pitch/loader (return undefined)
};

Modifying the Loader Chain

During pitching, the loaders array is writeable:
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  // Add options to the next loader
  const nextLoader = this.loaders[this.loaderIndex + 1];
  if (nextLoader) {
    nextLoader.options = {
      ...nextLoader.options,
      customFlag: true
    };
  }
};
Modifying the loader chain is an advanced technique. Use with caution as it can make your loader harder to understand and maintain.

Complete Example: Custom Inline Loader

// inline-loader.js
const path = require('path');

module.exports = function(source) {
  // This won't be called if pitch returns
  return source;
};

module.exports.pitch = function(remainingRequest) {
  const callback = this.async();
  
  // Get the absolute path
  const resourcePath = this.resourcePath;
  
  // Check if we should inline this file
  this.fs.stat(resourcePath, (err, stats) => {
    if (err) return callback(err);
    
    // Inline small files
    if (stats.size < 8192) {
      this.fs.readFile(resourcePath, (err, content) => {
        if (err) return callback(err);
        
        // Return as base64 data URL
        const ext = path.extname(resourcePath).slice(1);
        const mimeType = getMimeType(ext);
        const base64 = content.toString('base64');
        
        callback(
          null,
          `module.exports = "data:${mimeType};base64,${base64}"`
        );
      });
    } else {
      // Continue normal processing for large files
      callback();
    }
  });
};

function getMimeType(ext) {
  const types = {
    'png': 'image/png',
    'jpg': 'image/jpeg',
    'svg': 'image/svg+xml'
  };
  return types[ext] || 'application/octet-stream';
}

Debugging Pitching

Log execution flow to understand pitching:
module.exports = function(source) {
  console.log('[NORMAL]', this.resourcePath);
  return source;
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  console.log('[PITCH]', {
    resource: this.resourcePath,
    remaining: remainingRequest,
    preceding: precedingRequest,
    loaderIndex: this.loaderIndex
  });
};

Best Practices

1. Use Pitching Sparingly

Most loaders don’t need pitching. Only use it when you need to:
  • Short-circuit the chain
  • Process loader output inline
  • Share data between phases

2. Document Pitching Behavior

/**
 * This loader uses pitching to inline small files.
 * Files < 8KB are converted to base64 data URLs during the pitch phase.
 * Larger files continue through the normal loader chain.
 */
module.exports.pitch = function(remainingRequest) {
  // ...
};

3. Handle Both Phases

If you use pitching, handle both phases gracefully:
module.exports = function(source) {
  // Normal phase (for files not handled in pitch)
  return transform(source);
};

module.exports.pitch = function(remainingRequest) {
  // Pitch phase (optional short-circuit)
  if (shouldShortCircuit()) {
    return generateCode();
  }
  // Continue to normal phase
};

4. Test Thoroughly

Test both short-circuit and normal execution paths:
it('should short-circuit for small files', async () => {
  const stats = await compile('small-file.png');
  expect(stats.modules[0].source).toContain('data:');
});

it('should use normal phase for large files', async () => {
  const stats = await compile('large-file.png');
  expect(stats.modules[0].source).not.toContain('data:');
});

Common Patterns

Pattern 1: Inline Require

module.exports.pitch = function(remainingRequest) {
  return `
    var content = require(${JSON.stringify('!!' + remainingRequest)});
    module.exports = processContent(content);
  `;
};

Pattern 2: Conditional Short-Circuit

module.exports.pitch = function(remainingRequest) {
  if (this.resourceQuery.includes('inline')) {
    return inlineContent(this.resourcePath);
  }
  // Continue normal execution
};

Pattern 3: Metadata Collection

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.startTime = Date.now();
  data.loaderCount = this.loaders.length;
};

module.exports = function(source) {
  const duration = Date.now() - this.data.startTime;
  this.getLogger().info(`Processed in ${duration}ms`);
  return source;
};

Pitfall: Infinite Loops

Be careful not to create infinite loops:
// ❌ Bad - infinite loop!
module.exports.pitch = function(remainingRequest) {
  // This creates an infinite loop
  return `require(${JSON.stringify(remainingRequest)})`;
};

// ✅ Good - use prefix to disable loaders
module.exports.pitch = function(remainingRequest) {
  return `require(${JSON.stringify('!!' + remainingRequest)})`;
};
The !! prefix disables all loaders (both pre and normal), preventing the loop.

Prefix Meanings

  • ! - Disable normal loaders
  • !! - Disable all loaders (pre, normal, post)
  • -! - Disable pre and normal loaders
module.exports.pitch = function(remainingRequest) {
  // Use the appropriate prefix
  return `
    // Disable all loaders
    var content = require(${JSON.stringify('!!' + remainingRequest)});
    
    // Or disable only normal loaders
    var content2 = require(${JSON.stringify('!' + remainingRequest)});
  `;
};

See Also