🐛🔨 The Contributors Guide to webpack — Part 2 🎨 🖼

Diving Deeper: Tapable, plugins and design patterns!

Sean T. Larkin
webpack
Published in
5 min readMay 30, 2017

--

The deeper we dive into webpack, the more we will find that everything is abstracted into modular single purpose classes and utilities! Which flavor are you going to try?

The Contributors Guide to webpack is a multi-part publication series outlining the many ways that you can learn about, and contribute to the webpack open source project. You can read the first article in the series here!

🌊 Diving Deeper 🌊

Although this and the previous guides are about contributing to webpack as a whole, I want to spend time covering some paradigms you will see across multiple projects first.

BONUS: For those who are interested in contributing to webpack core, the webpack resolver, or just want to start writing your own plugins, then this article is especially applicable for you!!

🍻 Tapable 🍻

As mentioned above, Tapable is the core building block for webpack. Class‘s and Object‘s that extend Tapable are known as “tapable instances”. Here is a very simplified example seen below:

const Tapable = require('tapable');class Compiler extends Tapable {
constructor() {
this.foo = "43";
this.applyPluginsAsync("run", this, function(err, done) {
if (done) {
this.doRunStuffAfterSuccess()}
}
this.applyPlugins("done", stats);
}
}

Tapable adds event emitting functionality that plugins can hook into. Hooks are easily identified by searching for this.applyPlugins in whatever codebase you are working in.

🔎 Anatomy of a plugin 🔍

The shape of a [webpack] Plugin is best described as a class that implements an apply() method. This method is called by the tapable instance to initialize the plugin’s event hooks. Take the following example:

class MyFirstPlugin {
apply(compiler) {
compiler.plugin("run", function(compiler, cb) {
console.log("webpack is about to start bundling");
cb(); // cb() signals to the compiler that this asnyc hook is finished.
});
compiler.plugin("done", function(stats) {
console.log("webpack is finished bundling");
});
}
}

There are a few things going on here:

  • The compiler instance is passed into the apply() method. This gives you access to the tapable instance so you can “tap” into the events it emits.
  • To hook into an event, one must call .plugin() on the tapable instance, and then specify the name of the event and a callback function where we will write our custom logic. IE: .plugin("eventName", function(){})
  • If the event is asynchronous, the last parameter received from the event callback, will be a callback hook which signals when our custom logic is completed.

How to register a plugin

A tapable instance can register a plugin, by calling apply() and passing in a `new` instance of that plugin. For example, to register the example plugin above, we would write compiler.apply(new MyFirstPlugin());

Design patterns with plugins

[webpack] Plugins are designed to be single purpose. You will often find this pattern throughout the core source code. Observe the example below:

You can find the real source code here on GitHub!

EntryOptionPlugin’s only purpose is to hook into the compiler to obtain all entry points defined in the configuration object and convert those entry values to SingleEntryPlugin instance or MultiEntryPlugin instance. These plugins are then passed back to the compiler to be registered with the apply() method.

Learning the source, writing your own plugins, asking questions, and/or take notes!! These are all incredible ways that you can not only build your own skills, but equip yourself to be the next webpack contributor!!! And at the end of the day, have fun!!

Tapable instances (classes you can plugin to) in webpack

Now that you have an idea of how webpack and its plugin system works, let’s look at the tapable instances that are used by webpack:

  • CompilerThe compiler runtime. Contains all of the top level compiler hooks. Plugins can hook into events like run and emit.
  • Compilation — The product of the webpack Compiler class. The Compiler returns Compilation‘s. This represents the entire dependency graph for your application! Compilation contains hooks like optimize-modules, seal and optimize-chunk-assets. Any time optimizations and overall full program transforms and utilities will be performed on the Compilation.
  • Resolver(s) — webpack resolvers are created from enhanced-resolve. Plugins that tap into the webpack resolvers, are used to customize the module resolution strategy. Any property inside of the resolve object in your webpack configuration corresponds to specific resolver plugins that are applied.
  • NormalModuleFactory — The NormalModuleFactory is the glue that binds the resolver, loaders, and the creation of the NormalModule instances together. Lifecycle hooks that you would plugin to may include before-resolve, after-resolve, and create-module. In addition to this, loaders are run against every module and transforms them from their current file format into something that can be added into a webpack Chunk.
  • ContextModuleFactory — The purpose of the ContextModuleFactory is identical to the NormalModuleFactory except it is for ContextModules a special webpack module type that enables “dynamic” requires.
  • Template — Because webpack performs code generation when it bundles, the Template instance is responsible for binding module data (via multiple template subclasses) to generate the shape and structure of the output bundles produced.
  • Template Subclasses — There are many levels of templates: MainTemplate (the runtime bundle wrapper), andChunkTemplate (the template that controls features likes the shape and format of the Chunk wrapper itself). Each source abstraction will contain its own template as well ( ModuleTemplate , DependencyTemplate ). Because of this, every Template subclass can be tapped into with module, render, and package hooks.
  • ParserThe webpack Parser is one of the most unique tapable instances in the webpack source code. The parser instance is a tapable instance powered by acorn AST parser. A reference to the parser lives on every NormalModule instance. Plugins that use the parser will utilize hooks for every lexical symbol type for the AST. Any of the plugins in our webpack core source code ending in ParserPlugin hooks into these events.

Any of the aforementioned classes can emit Tapable events and therefore have plugins written for them!

Tapable classes return a variety of data and information from the webpack lifecycle.

Now what?

With the information you have learned, pick one of the following tapable instances, and goto http://github.com/webpack/webpack and find a plugin written for that instance. Starting from somewhere that is familiar and understandable and branching out is one of the best ways to dive into a new codebase!! Then feel free to take a look at our work-in-progress Plugin API documentation for more examples!

Learning the source, writing your own plugins, asking questions, and/or take notes!! These are all incredible ways that you can not only build your own skills, but equip yourself to be the next webpack contributor!!! And at the end of the day, have fun!!

Stay tuned for our next installment as we put both Part 1, and Part 2 together to bring you the overall “System Design”!!

No time to help contribute? Want to give back in other ways? Become a Backer or Sponsor to webpack by donating to our open collective. Open Collective not only helps support the Core Team, but also supports contributors who have spent significant time improving our organization on their free time! ❤

--

--

Sean T. Larkin
webpack

@Webpack Core team & AngularCLI team. Program Manager @Microsoft @EdgeDevTools. Web Performance, JavaScripter, Woodworker, 🐓 Farmer, and Gardener!