Parcel 21 ๐ฆ Plugin Architecture โ Resolvers, transformers, bundlers & optimizers
Parcel’s plugin architecture is not a single extension point. It is a staged pipeline, and each stage has its own plugin type with a strictly defined contract. The build runs through seven phasesโresolving, transforming, bundling, naming, packaging, optimizing, and compressingโand each phase is extensible through a different plugin interface . The four plugin types that shape the bundle most directly are the resolver, the transformer, the bundler, and the optimizer. They are not interchangeable, and they are not configured in the same way. The resolver is an array in .parcelrc, the transformer is a glob map, the bundler is a single plugin name, and the optimizer is a glob map that runs over the finished bundle .
The distinction between a transformer and an optimizer is the unit of work. A transformer receives a single assetโa TypeScript file, a CSS file, an SVGโand returns a transformed asset. An optimizer receives an entire bundleโthe concatenated output of the packaging phaseโand returns a transformed bundle . This is why minification is an optimizer and TypeScript compilation is a transformer. The optimizer sees the whole bundle, which is what tree-shaking and minification require. The transformer sees one file at a time, which is what compilation requires.
Key point: Resolvers turn import specifiers into absolute paths. Transformers compile individual assets and discover dependencies. Bundlers group assets into bundles. Optimizers transform the finished bundles. Each has a different configuration shape in .parcelrc, and the "..." syntax extends the default plugin chain rather than replacing it.
Why the plugin types are separated
The unit-of-work problem. A plugin that compiles TypeScript needs to see one file at a time. A plugin that minifies JavaScript needs to see the entire bundle. If both were the same plugin type, the TypeScript plugin would receive a bundle it cannot compile, and the minifier would receive a single file it cannot tree-shake. The transformer/optimizer split is the mechanism that gives each plugin the unit it needs .
The configuration problem. The four plugin types are configured differently because they match against different things. The resolver matches against the import specifier (the text in the import statement). The transformer matches against the file path. The bundler is a single plugin that receives the entire asset graph. The optimizer matches against the output bundle filename . A single configuration shape could not express all four matching rules.
The experimental status of the bundler. The bundler plugin is marked experimental in the Parcel documentation, and the documentation warns that the API is “subject to change, even between minor updates” . The resolver, transformer, and optimizer are stable. The bundler is the extension point for changing how assets are grouped, and it is the one that a typical project does not need to touch. The default bundler handles the common cases, and the custom bundler is for vendoring, module federation, or other grouping strategies that the default does not express.
The trade-off. The four-type separation adds configuration surface. A project that needs to compile a new file format uses a transformer; a project that needs to optimize the output uses an optimizer; a project that needs a custom import syntax uses a resolver. The plugin type is determined by the phase the plugin runs in, not by what the plugin does. The separation is the cost of the pipeline model.
a. Resolvers: turning specifiers into paths
A resolver plugin receives a dependencyโthe asset that contains the import and the specifier being importedโand returns an absolute file path or null to pass to the next resolver. Resolvers are configured as an array in .parcelrc, and they run in order until one returns a result .
// .parcelrc โ a custom resolver before the default
{
"extends": "@parcel/config-default",
"resolvers": ["@company/parcel-resolver", "..."]
}
The "..." in the array is the default resolver. If the custom resolver returns null for a specifier it does not understand, the default resolver handles it. If the "..." is omitted, the custom resolver must handle every dependency in the project, or the build fails . The resolver is the plugin type for a custom import syntax, a virtual module, or a different module resolution algorithm.
// A minimal resolver plugin
import { Resolver } from '@parcel/plugin';
export default new Resolver({
async resolve({ dependency }) {
// dependency.specifier is the text in the import statement.
// dependency.resolveFrom is the file that contains the import.
if (dependency.specifier.startsWith('^/')) {
const path = dependency.specifier.replace('^/', '/absolute/');
return { filePath: path };
}
// Return null to pass to the next resolver in the chain.
return null;
},
});
The resolver’s return value is a filePath, which is the absolute path to the asset. The resolver does not read the file; it only determines where the file is. The transformer reads the file and compiles it .
b. Transformers: compiling assets and discovering dependencies
A transformer plugin receives a single asset, compiles it, and returns the transformed asset along with any dependencies it discovered. Transformers are configured as a glob map in .parcelrc, and multiple transformers can run in series over the same asset .
// .parcelrc โ a TypeScript transformer
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
"icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]
}
}
The glob map is ordered. The icons/*.svg entry is matched before the *.svg entry, so the icon-specific transformer runs first, and the "..." runs the default SVG transformer afterward. If the "..." is omitted, the default transformer is replaced, and the SVG may not be processed correctly .
// A minimal transformer plugin
import { Transformer } from '@parcel/plugin';
export default new Transformer({
async transform({ asset }) {
// asset.getCode() returns the source code.
const code = await asset.getCode();
// asset.setCode() sets the transformed code.
asset.setCode(code.replace(/__VERSION__/g, '1.0.0'));
// asset.addDependency() adds a dependency that Parcel will resolve.
asset.addDependency({
specifier: './injected.js',
specifierType: 'esm',
});
return [asset];
},
});
The transformer’s job is not just to transform the code. It is also to discover the dependencies. A TypeScript transformer discovers the imports in the TypeScript file and adds them as dependencies, so the resolver can resolve them and the transformer can process them . The asset graph is built from the dependencies that the transformers discover.
c. Bundlers and optimizers
The bundler plugin receives the entire asset graph and modifies it to add bundle nodes that group assets into output bundles. It is configured as a single plugin name, not a glob map, and it is experimental .
// .parcelrc โ a custom bundler
{
"extends": "@parcel/config-default",
"bundler": "@company/parcel-bundler"
}
The bundler is the plugin type for changing the grouping strategyโsplitting app and vendor code, or creating a bundle for a specific deployment target. The default bundler handles the common cases, and the custom bundler is for the cases the default does not express.
The optimizer plugin receives a bundle and its source map, and returns the transformed contents and map. Optimizers are configured as a glob map, and multiple optimizers can run in series .
// .parcelrc โ a custom optimizer
{
"extends": "@parcel/config-default",
"optimizers": {
"*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"]
}
}
The optimizer is the plugin type for minification, tree-shaking, and any transformation that needs the whole bundle. The @parcel/optimizer-swc is the default minifier in recent Parcel versions, replacing Terser . A custom optimizer that runs before the default can prepend a license header or transform the bundle in a way that the minifier then processes.
Complete Example Session
// ============================================
// PART 1: THE RESOLVER
// ============================================
// .parcelrc
{
"extends": "@parcel/config-default",
"resolvers": ["@company/parcel-resolver", "..."]
}
// The resolver handles the "^/" specifier.
// The "..." is the default resolver for everything else.
// ============================================
// PART 2: THE TRANSFORMER
// ============================================
// .parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
"icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]
}
}
// The TypeScript transformer compiles .ts and .tsx files.
// The icon transformer runs before the default SVG transformer.
// ============================================
// PART 3: THE BUNDLER
// ============================================
// .parcelrc
{
"extends": "@parcel/config-default",
"bundler": "@company/parcel-bundler"
}
// The custom bundler receives the asset graph.
// It groups the assets into bundles.
// The bundler is experimental.
// ============================================
// PART 4: THE OPTIMIZER
// ============================================
// .parcelrc
{
"extends": "@parcel/config-default",
"optimizers": {
"*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"]
}
}
// The custom optimizer runs first.
// The SWC minifier runs second.
// The result of the first optimizer is passed to the second.
// ============================================
// PART 5: THE PLUGIN NAMING CONVENTION
// ============================================
// Official: @parcel/{type}-{name}
// Community: parcel-{type}-{name}
// Scoped: @scope/parcel-{type}[-{name}]
//
// The {type} is the plugin type: resolver, transformer, bundler, optimizer.
// The {name} is descriptive of the plugin's purpose.
// ============================================
// PART 6: THE "..." SYNTAX
// ============================================
// The "..." embeds the next-priority plugin chain.
// In a resolver array: ["custom", "..."] runs custom, then default.
// In a transformer glob: ["custom", "..."] runs custom, then default.
// Without "...", the default is replaced.
// ============================================
// PART 7: THE TRANSFORMER'S DEPENDENCY DISCOVERY
// ============================================
// A transformer that adds a dependency:
import { Transformer } from '@parcel/plugin';
export default new Transformer({
async transform({ asset }) {
asset.addDependency({
specifier: './injected.js',
specifierType: 'esm',
});
return [asset];
},
});
// The resolver resolves the dependency.
// The transformer processes the resolved file.
// ============================================
// PART 8: THE OPTIMIZER'S CONDITIONAL RUN
// ============================================
// An optimizer that only runs in production:
import { Optimizer } from '@parcel/plugin';
export default new Optimizer({
async optimize({ contents, map, bundle }) {
if (!bundle.env.shouldOptimize) {
return { contents, map };
}
return { contents: minify(contents), map };
},
});
// The optimizer checks the environment before optimizing.
// ============================================
// PART 9: THE PLUGIN CONTRACT
// ============================================
// All Parcel plugins are stateless and pure.
// The same input must produce the same output.
// No side effects, no implicit dependencies.
// The cache depends on the purity.
// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================
// [ ] The resolver is in the resolvers array, with "..." if it is not universal.
// [ ] The transformer is in the transformers glob map, with "..." if it extends the default.
// [ ] The bundler is a single plugin name.
// [ ] The optimizer is in the optimizers glob map.
// [ ] The plugin names follow the naming convention.
// [ ] The plugins are stateless and pure.
// [ ] The "..." syntax is used where the default behavior should be preserved.
The ten parts covered the resolver, the transformer, the bundler, the optimizer, the naming convention, the "..." syntax, the dependency discovery, the conditional optimization, the plugin contract, and the verification. The through-line is that each plugin type has a different unit of work and a different configuration shape.
Quick Reference
The four plugin types
| Plugin | Unit of work | Configuration | Stable? |
|---|---|---|---|
| Resolver | A dependency specifier | Array in .parcelrc | Yes |
| Transformer | A single asset | Glob map in .parcelrc | Yes |
| Bundler | The asset graph | Single name in .parcelrc | Experimental |
| Optimizer | A finished bundle | Glob map in .parcelrc | Yes |
The naming convention
| Type | Official | Community | Scoped |
|---|---|---|---|
| Resolver | @parcel/resolver-{name} | parcel-resolver-{name} | @scope/parcel-resolver[-{name}] |
| Transformer | @parcel/transformer-{name} | parcel-transformer-{name} | @scope/parcel-transformer[-{name}] |
| Bundler | @parcel/bundler-{name} | parcel-bundler-{name} | @scope/parcel-bundler[-{name}] |
| Optimizer | @parcel/optimizer-{name} | parcel-optimizer-{name} | @scope/parcel-optimizer[-{name}] |
The "..." syntax
| Location | With "..." | Without "..." |
|---|---|---|
| Resolvers | Custom runs, default handles the rest | Custom must handle everything |
| Transformers | Custom runs, then default | Custom replaces default |
| Optimizers | Custom runs, then default | Custom replaces default |
Best Practices
โ Do This:
// Use "..." to extend the default resolver chain
"resolvers": ["@company/parcel-resolver", "..."] // โ
// Use "..." to extend the default transformer
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"] // โ
// Check shouldOptimize in an optimizer
if (!bundle.env.shouldOptimize) return { contents, map }; // โ
// Follow the naming convention
"@company/parcel-transformer-svg-icons" // โ
โ Don’t Do This:
// Omit "..." from a resolver that does not handle everything
"resolvers": ["@company/parcel-resolver"] // build may fail // โ
// Use a local file path in a published config package
"./local-transformer.js" // rejected in config packages // โ
// Make an optimizer stateful
// The plugin may run in multiple workers. // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The build fails on an unresolved import | The custom resolver omitted "..." | Add "..." to the array |
| The default transformer is not running | The "..." is missing from the glob | Add "..." to the pipeline |
| The plugin is not found | The name does not follow the convention | Use the correct prefix |
| The optimizer runs in development | shouldOptimize not checked | Add the check |
| The cache is stale | The plugin has side effects | Make the plugin pure |
Real-World Examples
1. Custom resolver
"resolvers": ["@company/parcel-resolver", "..."]
2. TypeScript transformer
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
3. SVG transformer with default
"icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]
4. Custom bundler
"bundler": "@company/parcel-bundler"
5. Custom optimizer
"optimizers": { "*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"] }
6. Optimizer conditional
if (!bundle.env.shouldOptimize) return { contents, map };
7. Dependency discovery
asset.addDependency({ specifier: './injected.js', specifierType: 'esm' });
8. Resolver returning null
return null; // pass to the next resolver
9. Naming
@company/parcel-transformer-svg-icons
10. Purity
// No state, no side effects, same input โ same output
Visual
The Parcel Phases and Plugins
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PHASES AND PLUGIN TYPES โ
โ โ
โ Resolving โโโถ Resolver plugin โ
โ โ โ
โ โผ โ
โ Transforming โโโถ Transformer plugin โ
โ โ โ
โ โผ โ
โ Bundling โโโถ Bundler plugin โ
โ โ โ
โ โผ โ
โ Naming โโโถ Namer plugin โ
โ โ โ
โ โผ โ
โ Packaging โโโถ Packager plugin โ
โ โ โ
โ โผ โ
โ Optimizing โโโถ Optimizer plugin โ
โ โ โ
โ โผ โ
โ Compressing โโโถ Compressor plugin โ
โ โ
โ Each phase has its own plugin type and contract. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Transformer vs Optimizer
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE UNIT OF WORK โ
โ โ
โ Transformer Optimizer โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ One asset โ โ One bundle โ โ
โ โ (TypeScript file) โ โ (concatenated JS) โ โ
โ โ โ โ โ โ
โ โ Compiles, discovers โ โ Minifies, tree- โ โ
โ โ dependencies โ โ shakes, prepends โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The transformer sees the file. โ
โ The optimizer sees the bundle. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The "..." Syntax
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ EXTENDING VS REPLACING โ
โ โ
โ With "..." Without "..." โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Custom plugin runs โ โ Custom plugin runs โ โ
โ โ then default runs โ โ default is gone โ โ
โ โ โ โ โ โ
โ โ Safe extension โ โ Full replacement โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Use "..." when the default behavior should be preserved. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Resolver | Turns specifiers into paths; array in .parcelrc |
| Transformer | Compiles assets; glob map in .parcelrc |
| Bundler | Groups assets; single name; experimental |
| Optimizer | Transforms bundles; glob map in .parcelrc |
The "..." syntax | Extends the default chain; omitting it replaces |
| Plugin purity | Stateless, pure, no side effects |
| Naming convention | @parcel/{type}-{name} or parcel-{type}-{name} |
Key takeaways:
- The plugin type is determined by the phase, not by the task. A resolver runs in the resolving phase, a transformer in the transforming phase, a bundler in the bundling phase, and an optimizer in the optimizing phase. The phase determines the unit of work and the configuration shape .
- Resolvers and transformers are configured differently. The resolver is an array that runs in order until one returns a result. The transformer is a glob map that matches the file path. The
"..."syntax extends the default chain in both cases, but the shape is different . - The bundler is experimental. The documentation warns that the bundler API may change between minor versions. The default bundler handles the common cases, and the custom bundler is for the cases it does not express .
- The optimizer receives the whole bundle. This is what distinguishes it from the transformer, which receives a single asset. Minification and tree-shaking are optimizers because they need the whole bundle. Compilation is a transformer because it needs one file at a time .
- Plugins must be pure and stateless. The plugin may run in multiple workers that cannot communicate. The cache depends on the plugin producing the same output for the same input with no side effects .
Remember: Parcel’s plugin architecture is a staged pipeline, and each stage has its own plugin type. The resolver turns specifiers into paths. The transformer compiles assets and discovers dependencies. The bundler groups assets into bundles. The optimizer transforms the finished bundles. The four types are not interchangeable, and they are not configured the same way. The "..." syntax is the extension mechanism: it preserves the default behavior while inserting the custom plugin. The plugin is pure and stateless, and the cache depends on it. The bundler is experimental, and the resolver, transformer, and optimizer are stable.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!