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:
- Pitching phase: Left to right (or top to bottom)
- 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:
- Remaining pitches are skipped
- The resource file is not read
- 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);
};
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?
style-loader needs the output of css-loader, not the input
- By returning in the pitch phase, it can inline a
require() call
- The
require() goes through the remaining loaders (css-loader)
- 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
};
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