Why there is no order, and how to deal with that
A Babel config looks something like this:
{
presets: ["preset-1", "preset-2"],
plugins: ["plugin-3"]
}
Since presets are essentially just a list of plugins, internally Babel reduces this config down to something like this:
{
plugins: ["plugin-1", "plugin-2", "plugin-3"]
}
It's natural to think that this becomes the order of which plugins run, but this is not the case. There's more steps than that.
Plugins at the top level look like this:
export function plugin(babel) {
return {
visitor: {
ClassDeclaration(path) {
// ...
},
FunctionDeclaration(path) {
// ...
},
},
};
};
Notice the visitor
here. A
visitor is an
object with methods defined on it that is used as part of a tree traversal.
A tree traversal is the process of exploring a large tree-like data structure. In this case our data structure is an AST.
An AST (or "Abstract Syntax Tree") is an abstract representation of code. It looks something like this:
{
type: "Program",
body: [{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "foo"
}
}, {
type: "ClassDeclaration",
id: {
type: "Identifier",
name: "Bar"
},
...
}]
}
Notice each of the objects with a type
property. These are AST
"Nodes". These nodes are what our plugin visitors are visiting.
As Babel runs, it traverses down the tree and calls plugin visitor methods on each of the matching nodes in the tree.
So what order does this happen in?
Well imagine if we did enforce plugin order. What we'd need to do is take one plugin traverse through the entire tree and run all the visitor methods and then go back to the top of the tree and start over with the next plugin.
We'd kick off a new traversal for every single plugin, each plugin getting the results of the last
The problem with that, is it'd be painfully slow.
Imagine you needed to fire off 1,000 different HTTP requests. If you fired them one after the other, waiting for the previous request to resolve before creating the next one, you'd be able to enforce the order.
However, that'd be the slowest possible way you could make all of those requests. If you instead ran them all at once in parallel. The whole process would only take as long as the slowest request.
The problem then would be if these requests conflicted with one another. If one request was deleting a resource that another was trying to get, it would be a race. If the resource was deleted before the other one could get it, then it will fail. The order of these requests still matters.
The same is true for plugin visitors. We can make them all much fast by running them all in the same traversal process. As we traverse up and down the AST, we call methods from different visitors at different times just based on whatever happens to come first.
This is why there's no true "order" of plugins at the top level. They all run at the "same" time.
Note that there is still an order at the individual node level. If there is
two visitors that are trying to reach a FunctionDeclaration
Babel will still run them in the order that they were configured at.
As much as possible, plugins should try not to care about the order that they run in. Oftentimes they can be written in a way that makes them "dumb" and just deals with whatever they are handed.
But when they can't, they can also be written in a way where they beat other plugins in the race to a node.
Remember how we're traversing down an AST one node at a time. If two plugins are trying to reach a node at the same time, one of them might decide to visit a node higher in the tree and traverse back down itself.
So imagine we have two plugins: One which is modifying a
ClassDeclaration
and one that is replacing a
ClassDeclaration
and getting rid of it in the process. We need
to run the "modifying" plugin first so that it has access to the
ClassDeclaration
.
Instead of visiting the ClassDeclaration
what if we visited the
top-level Program
node instead?
export function modifyingPlugin(babel) {
return {
visitor: {
Program(programPath) {
programPath.traverse({
ClassDeclaration(classPath) {
// ...
},
});
},
},
};
};
Now our plugin is forcing itself to reach the ClassDeclaration
when it reaches the Program
which we know happens first.
The problem that we have now is what happens when we have multiple plugins that need to run "first"?
If we have two plugins that are both trying to force themselves to be first
by going straight to the Program
we have the same problem again.
There's no real "fix" to this problem. Even if we bit the performance bullet and ran them all in sequence, that'd only make configuration harder because now plugin order matters all of the time instead of just some of the time (there's other problems too, but they get really complicated fast).
Plugins can help alleviate this problem by being less destructive and acting "dumber" so that they aren't conflicting with one another.
Babel can also help make this easier for users by allowing plugins to specify "order". Where they can explicitly say "I need to run before/after Plugin X". Check out this Babel PR for more info.