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