Authoring Plugins

What to keep in mind when authoring a plugin

Plugin APIs

#

There are several different types of plugins. They all look very similar, but are kept separate so we can have strict contracts on what each one is allowed to do.

There are some rules that should be followed across every type of plugin:

The plugin APIs all follow a common shape:

import { NameOfPluginType } from "@parcel/plugin";

export default new NameOfPluginType({
async methodName(opts: JSONObject): Promise<JSONObject> {
return result;
},
});

Each method of a plugin is an async function that:

Methods may also return their result directly rather than a promise.

If something you need is not being passed through opts, please come talk to the Parcel team about it. Avoid trying to get information yourself from other sources, especially from the file system.

Plugins can also be written in Rust, Go, Swift, or any other language that can produce a shared library with a C ABI. See Native plugins for details.

JavaScript runtime

#

JavaScript plugins do not run in Node.js. Parcel includes an embedded JavaScript engine (QuickJS), and runs plugins inside it on each of its worker threads. This means you don't need Node installed to use JavaScript plugins, but there are some differences to be aware of.

Parcel emulates the subset of Node's built-in modules that plugins and the compilers they wrap commonly need: assert, buffer, console, constants, crypto, events, fs, fs/promises, module, os, path, process, punycode, querystring, stream, string_decoder, tty, url, util, vm, and zlib. Each may also be imported with the node: prefix. Globals such as process, Buffer, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, structuredClone, and crypto are available as well.

Some things are not supported:

npm dependencies of your plugin are loaded from node_modules as usual, as long as they only use the supported APIs.

Module formats

#

Parcel supports plugins written as either CommonJS or ES modules. The module format of a plugin is determined from either the file extension of the resolved module, or from its package.json. Use the .mjs extension for ES module plugins, and either .cjs or .js for CommonJS. If "type": "module" is declared in the plugin's package.json, the .js extension is treated as ESM instead of CommonJS. This behavior matches how Node.js loads modules.

Plugins can also be written in TypeScript. Files with a .ts or .tsx extension are loaded directly, with their types stripped, so a local plugin does not need to be compiled first. The @parcel/plugin package includes TypeScript types for all of the plugin APIs.

Configuration

#

Plugins can accept configuration from the user's project. This is specified in the .parcelrc file, by using an object with plugin and config properties in place of the plugin name:

.parcelrc:
{
"extends": "@parcel/config-default",
"transformers": {
"*.svg": [
{
"plugin": "parcel-transformer-foo",
"config": {
"bar": true
}
}
]
}
}

The config is passed to every plugin method as the config option. It is undefined if no config was provided.

import { Transformer } from "@parcel/plugin";

export default new Transformer({
async transform({ asset, config }) {
if (config?.bar) {
// ...
}
},
});

Because the same plugin may appear multiple times in a .parcelrc with different configuration, avoid storing config in module-level state.

Invalidation

#

Some plugins wrap a compiler or tool which has its own config loading mechanism built in (e.g. Babel's babel.config.json), or read other files from the file system. Parcel automatically tracks all of the files that JavaScript plugins read with the fs module, along with the source files of the plugins themselves and all of their dependencies. When one of these files changes, Parcel rebuilds the whole project, since the plugin's results may depend on it.

When a transformer needs to resolve a dependency itself (e.g. to inline an imported file), use the resolve function passed to transform. This resolves a specifier using the resolvers configured in .parcelrc, exactly as a dependency of the asset would be, and only invalidates the asset being transformed when the result would change. Like a dependency, it accepts an importType option describing how the file will be used. A file resolved as javascript (the default) must have a transformer that turns it into JavaScript, so pass importType: 'url' to resolve any file, e.g. one you will read yourself. It returns {filePath, pipeline, query, sideEffects}, {isExternal: true} or {isExcluded: true}, or null when no resolver resolves the specifier, and rejects with the resolvers' diagnostics when one fails.

import { Transformer } from "@parcel/plugin";
import fs from "fs";

export default new Transformer({
async transform({ asset, resolve }) {
let { filePath } = await resolve(asset.filePath, "./included.txt", {
importType: "url",
});
let contents = fs.readFileSync(filePath, "utf8");
// ...
},
});

Naming

#

All plugins must follow a naming system:

Official package Community packages Private company/scoped team packages
Configs @parcel/config-{name} parcel-config-{name} @scope/parcel-config[-{name}]
Resolvers @parcel/resolver-{name} parcel-resolver-{name} @scope/parcel-resolver[-{name}]
Transformers @parcel/transformer-{name} parcel-transformer-{name} @scope/parcel-transformer[-{name}]
Namers @parcel/namer-{name} parcel-namer-{name} @scope/parcel-namer[-{name}]
Optimizers @parcel/optimizer-{name} parcel-optimizer-{name} @scope/parcel-optimizer[-{name}]
Reporters @parcel/reporter-{name} parcel-reporter-{name} @scope/parcel-reporter[-{name}]

The {name} must be descriptive and directly related to the purpose of the package. Someone should be able to have an idea of what the package does simply by reading the name in a .parcelrc or package.json#devDependencies.

parcel-transformer-posthtml
parcel-optimizer-svgo
parcel-reporter-graph-visualizer

If your plugin adds support for a specific tool, please use the name of the tool.

parcel-transformer-es6 (bad)
parcel-transformer-babel (good)

If your plugin is a reimplementation of something that exists, try naming it something that explains why it is a separate:

parcel-transformer-better-typescript (bad)
parcel-transformer-typescript-server (good)

We ask that community members work together and when forks happen to try and resolve them. If someone made a better version of your plugin, please consider giving the better package name over, have them make a major version bump, and redirect people to the new tool.

See Local plugins for recommendations on using plugins in your project without publishing them.

Versioning

#

You must follow semantic versioning (to the best of your ability). No, it's not the perfect system, but it's the best one we have and people do depend on it.

If plugin authors intentionally don't follow semantic versioning, Parcel may start warning users that they should be locking down the version number for your plugin.

Engines

#

You must specify a package.json#engines.parcel field with the version range of Parcel that your plugin supports:

{
"name": "parcel-transformer-imagemin",
"engines": {
"parcel": "^3.0.0-0"
}
}

If you do not specify this field, Parcel will output a warning:

Warning: The plugin "parcel-transformer-typescript" needs to specify a
`package.json#engines.parcel` field with the supported Parcel version range.

If you do specify the parcel engine field and the user is using an incompatible version of Parcel, they will see an error:

Error: The plugin "parcel-transformer-typescript" is not compatible with the
current version of Parcel. Requires "2.x" but the current version is "3.0.0".

Parcel uses npm's semver rules to match version ranges. Prerelease versions of Parcel (e.g. 3.0.0-alpha.1) only match ranges that include a prerelease, so use ^3.0.0-0 rather than ^3.0.0 or 3.x to support them.

Native plugins are also checked against the ABI version declared in their parcel.abi field. See Native plugins for details.