Parcel 23 ๐ฆ Writing a Transformer โ Custom asset type support
A transformer is a plugin that receives a single asset, compiles it, and returns one or more transformed assets. The unit of work is one file at a time, and the output is an asset whose type property may have changed. The custom asset type support is the mechanism that makes the transformer useful beyond the built-in file types: a transformer can set asset.type to a new value, and Parcel will re-evaluate the pipeline for that asset against the new type. A TypeScript transformer sets the type to js, and the asset is then processed by the JavaScript pipeline. A custom transformer can set the type to mytype, and a second transformer registered for mytype will process the output .
The transformer API is defined by the @parcel/plugin package. The plugin is created with new Transformer({ async transform({ asset, ... }) { ... } }), and the asset object is a MutableAsset that provides getCode(), setCode(), addDependency(), and the type property. The transformer can read the source, compile it, change the type, add dependencies that the resolver should process, and return the transformed asset or a list of assets . The API is stateless and pure: the same input must produce the same output, and the plugin must not rely on side effects.
Key point: The transformer receives a MutableAsset, reads its content with getCode() or getBuffer(), compiles it, sets the new content with setCode(), and changes the type property if the output is a different format. The new type determines which pipeline processes the asset next. The "..." syntax in .parcelrc runs the default chain after the custom transformer, which is required when the transformer does not change the type but wants the default processing to continue.
Why the transformer type change is the custom asset support
The type-driven pipeline. Parcel’s pipeline is type-driven. An asset with type ts is processed by the TypeScript transformer, and an asset with type js is processed by the JavaScript transformer. A custom transformer that produces a new format sets the type to the new format, and Parcel re-evaluates the pipeline for that asset. This is how TypeScript becomes JavaScript: the TypeScript transformer compiles the code, sets asset.type = 'js', and the JavaScript pipeline runs next . A custom transformer for a new language follows the same pattern: compile the code, set the type to js (or css, or json), and let the default pipeline handle the rest.
The named pipeline and the type. A named pipeline is a prefix in the import statement that selects a different chain for the same file type. The transformer’s type change is the other half of the same mechanism. A jsx: import of an SVG runs the JSX transformer, which sets the type to js. The asset is then processed by the JavaScript pipeline, not the SVG pipeline. The two mechanisms are complementary: the named pipeline selects the first chain, and the type change selects the next chain .
The dependency discovery. A transformer is not only a compiler. It is also a dependency discoverer. A TypeScript transformer reads the import statements and adds them as dependencies, so the resolver can resolve them and the transformer can process the imported files. A custom transformer that generates code with imports must add the dependencies explicitly with asset.addDependency(), and the resolver will process them . The dependency discovery is the mechanism that builds the asset graph.
The trade-off. The transformer is stateless and pure, which means it cannot cache its own results across builds. Parcel’s cache handles the transformation results, but the plugin must be pure for the cache to be correct. A transformer that reads a configuration file must use invalidateOnFileChange() to tell Parcel when the configuration changes, or the cache will return stale results . The purity and the explicit invalidation are the cost of the parallel, cached build.
a. The transformer structure and the type change
The transformer is created with the Transformer class from @parcel/plugin. The transform method receives an object with the asset property, and the method returns an array of assets.
// my-transformer.js โ a transformer that compiles a custom format to JavaScript
import { Transformer } from '@parcel/plugin';
export default new Transformer({
async transform({ asset }) {
// Read the source code.
const code = await asset.getCode();
// Compile the custom format to JavaScript.
const compiled = compileMyFormat(code);
// Set the new content.
asset.setCode(compiled);
// Change the type to js.
// The asset is now processed by the JavaScript pipeline.
asset.type = 'js';
// Return the asset.
return [asset];
},
});
The asset.type assignment is the custom asset support. A transformer that does not change the type and wants the default pipeline to continue must return [asset] and the .parcelrc must include "..." after the custom transformer . The "..." runs the default chain for the current type, which is the mechanism for a transformer that modifies the content without changing the format.
// my-transformer.js โ a transformer that adds a dependency
import { Transformer } from '@parcel/plugin';
export default new Transformer({
async transform({ asset }) {
const code = await asset.getCode();
// Add a dependency that the resolver will process.
// The specifier is relative to the asset's file path.
asset.addDependency({
specifier: './injected.js',
specifierType: 'esm',
});
// Set the new content.
asset.setCode(code.replace(/__VERSION__/g, '1.0.0'));
// The type is unchanged, so the default pipeline continues.
return [asset];
},
});
The addDependency call is the mechanism for a transformer that generates code with imports. The specifier is the text that the resolver will resolve, and the specifierType tells the resolver how to interpret it. The esm type treats the specifier as an ES module import, and the url type treats it as a URL .
b. The .parcelrc registration and the type pipeline
The transformer is registered in .parcelrc with a glob pattern that matches the source file type. The "..." runs the default chain if the transformer does not change the type.
// .parcelrc โ the transformer registration
{
"extends": "@parcel/config-default",
"transformers": {
// The custom transformer for the .myext files.
"*.myext": ["my-parcel-transformer"],
},
}
The transformer compiles the .myext file and sets the type to js. The asset is then processed by the *.js pipeline, which is the default JavaScript pipeline. The two pipelines are chained by the type change, not by the .parcelrc glob .
// .parcelrc โ a transformer that does not change the type
{
"extends": "@parcel/config-default",
"transformers": {
// The "..." runs the default transformer for the type
// after the custom transformer.
"*.js": ["my-parcel-transformer", "..."],
},
}
The "..." is the mechanism for a transformer that modifies the content but does not change the type. The custom transformer runs first, and the default JavaScript transformer runs second. Without the "...", the default transformer is replaced, and the asset may not be processed correctly .
Complete Example Session
// ============================================
// PART 1: THE CUSTOM FORMAT
// ============================================
// A .greeting file contains a name and a message.
// The transformer compiles it to a JavaScript module.
//
// greet.greeting:
// name: Alice
// message: Hello
// ============================================
// PART 2: THE TRANSFORMER
// ============================================
// greeting-transformer.js
import { Transformer } from '@parcel/plugin';
export default new Transformer({
async transform({ asset }) {
// Read the source code.
const code = await asset.getCode();
// Parse the custom format.
const lines = code.split('\n');
const data = {};
for (const line of lines) {
const [key, value] = line.split(': ');
data[key.trim()] = value.trim();
}
// Generate the JavaScript module.
const js = `export const name = ${JSON.stringify(data.name)};
export const message = ${JSON.stringify(data.message)};`;
// Set the content and the type.
asset.setCode(js);
asset.type = 'js';
return [asset];
},
});
// .parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"*.greeting": ["greeting-transformer"]
}
}
// index.js
import { name, message } from './greet.greeting';
console.log(`${message}, ${name}!`); // "Hello, Alice!"
The transformer reads the .greeting file, compiles it to a JavaScript module, sets the type to js, and the JavaScript pipeline processes the output. The import statement references the .greeting file, and the bundler resolves it through the custom transformer.
Quick Reference
The transformer API
| Method | Purpose |
|---|---|
asset.getCode() | Read the source as a string |
asset.getBuffer() | Read the source as a buffer |
asset.setCode(code) | Set the transformed content |
asset.setBuffer(buffer) | Set the transformed content as a buffer |
asset.type = 'js' | Change the asset type |
asset.addDependency(options) | Add a dependency for the resolver |
The .parcelrc patterns
| Pattern | Purpose |
|---|---|
"*.myext": ["my-transformer"] | Custom transformer, type change |
"*.js": ["my-transformer", "..."] | Custom transformer, then default |
The dependency options
| Option | Value | Purpose |
|---|---|---|
specifier | './injected.js' | The import specifier |
specifierType | 'esm' | ES module resolution |
specifierType | 'url' | URL resolution |
Best Practices
โ Do This:
// Set the type when the output is a different format
asset.type = 'js'; // โ
// Return the asset array
return [asset]; // โ
// Use "..." when the type is unchanged
"*.js": ["my-transformer", "..."] // โ
// Add dependencies for generated imports
asset.addDependency({ specifier: './x.js', specifierType: 'esm' }); // โ
โ Don’t Do This:
// Forget to set the type for a different format
asset.setCode(js); // the asset is still the original type // โ
// Use "..." when the type is changed
"*.myext": ["my-transformer", "..."] // the default .myext pipeline runs // โ
// Make the transformer stateful
// The plugin may run in multiple workers. // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The output is not processed by the default pipeline | The type is not changed | Set asset.type = 'js' |
| The default transformer does not run | The "..." is missing | Add "..." to the pipeline |
| The generated imports fail | The dependencies are not added | Use asset.addDependency() |
| The cache is stale | The transformer reads a file without invalidation | Use invalidateOnFileChange() |
Real-World Examples
1. Type change
asset.type = 'js';
2. Set the content
asset.setCode(compiled);
3. Add a dependency
asset.addDependency({ specifier: './x.js', specifierType: 'esm' });
4. The .parcelrc
"*.myext": ["my-transformer"]
5. The "..." for unchanged type
"*.js": ["my-transformer", "..."]
6. Read the source
const code = await asset.getCode();
7. Read the buffer
const buffer = await asset.getBuffer();
8. Return the asset
return [asset];
9. Invalidate on file change
asset.invalidateOnFileChange('config.json');
10. The compiled output
export const name = "Alice";
Visual
The Type Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE TYPE CHANGE SELECTS THE NEXT PIPELINE โ
โ โ
โ .greeting file โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ greeting-transformerโ Compiles to JavaScript โ
โ โ asset.type = 'js' โ Changes the type โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ *.js pipeline โ The default JavaScript pipeline โ
โ โ (default) โ processes the compiled output โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The final bundle โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The "..." for Unchanged Type
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WITHOUT AND WITH "..." โ
โ โ
โ Without "..." With "..." โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Custom transformer โ โ Custom transformer โ โ
โ โ (default replaced) โ โ (default runs next) โ โ
โ โ โ โ โ โ
โ โ The asset may not โ โ The default chain โ โ
โ โ be processed โ โ processes the asset โ โ
โ โ correctly โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Use "..." when the type is unchanged. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Transformer API | new Transformer({ async transform({ asset }) { ... } }) |
| Read source | asset.getCode() / asset.getBuffer() |
| Set content | asset.setCode() / asset.setBuffer() |
| Change type | asset.type = 'js' |
| Add dependency | asset.addDependency(options) |
| Registration | .parcelrc transformers glob |
"..." | Runs the default chain for the current type |
Key takeaways:
- The transformer receives one asset and returns one or more assets. The
MutableAssetprovides the read and write methods, and thetypeproperty is the mechanism for custom asset support. A transformer that compiles a new format to JavaScript setsasset.type = 'js', and the JavaScript pipeline processes the output . - The type change is the pipeline selector. Parcel’s pipeline is type-driven. The transformer changes the type, and Parcel re-evaluates the pipeline for the new type. A
.greetingfile becomes ajsasset, and the*.jspipeline runs next . - The
"..."is for transformers that do not change the type. A transformer that modifies the content but keeps the format must use["my-transformer", "..."]to run the default chain. Without the"...", the default transformer is replaced, and the asset may not be processed correctly . - The dependency discovery is part of the transformer’s job. A transformer that generates code with imports must add the dependencies with
asset.addDependency(). The resolver processes the dependencies, and the transformer processes the imported files . - The transformer is stateless and pure. The plugin may run in multiple workers that cannot communicate. The cache depends on the transformer producing the same output for the same input. A transformer that reads a configuration file must use
invalidateOnFileChange()to tell Parcel when the configuration changes .
Remember: A transformer is a compiler for a single asset. It reads the source, compiles it, sets the content, and changes the type if the output is a different format. The type change is the mechanism that selects the next pipeline. The "..." is the mechanism that runs the default chain when the type is unchanged. The dependencies are the mechanism that builds the asset graph. The transformer is pure and stateless, and the cache depends on it. The custom asset support is the type property, and the pipeline is the consumer.
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!