CLI tool
unjs/webpackbar avatar
unjs/webpackbar

webpackbar: the reporter layer behind the build bar

Elegant ProgressBar and Profiler for Webpack 3 , 4 and 5

2,094 stars66 forksTypeScriptMIT

At a glance

What is it?
webpackbar is a plugin for webpack and rspack that draws a progress bar and a build profiler while a build or watch session runs. Its reporter hooks are the part worth reading closely, and two of them fail quietly.
Who is it for?
Adopt webpackbar when a build runs long enough to stare at, when one process starts more than one compilation, or when you want output other than the packaged bars, and set `name` per instance while switching `fancy` and `basic` off as soon as you add your own reporter.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A plugin for build feedback, and the price of upkeep

A webpack build runs long enough that you want to know what it is doing while it is doing it. webpackbar is a plugin that attaches to that build and converts its lifecycle into a bar, a log line, or whatever a reporter of your own prints. The audience is narrow and easy to name: a developer sitting in watch mode, a server side render that fires several compilations at once, and anyone on Windows, which the README lists among six headline capabilities next to multi build bars and the profiler.

The footprint is small. The published package ships only `dist`, built from TypeScript sources in `src/` with unbuild, and it pulls in four runtime dependencies: `ansis`, `consola`, `pretty-time` and `std-env`. Of those, `std-env` is the one the options section leans on, since `fancy` and `basic` derive their defaults from the environment the build runs in.

Upkeep is scripted rather than manual. One script lints and runs the test suite, another publishes and pushes tags, and `renovate.json` at the repository root keeps the dependency ranges current. The last push was on 2026-10-05, while the published version is 7.0.0, tagged on 2024-11-06, so the default branch carries commits that no `npm install` will hand you. The code is under the MIT licence, which means a closed product bundling it still has to carry the notice.

Installing webpackbar and wiring the first build

Six install lines cover the managers people actually use, so take the one your project already standardises on:

bash
npx nypm install webpackbar
npm install webpackbar
yarn add webpackbar
pnpm install webpackbar
bun install webpackbar
deno install webpackbar

Then add the plugin to the webpack config. The README asks you to make sure the `webpack` peer dependency is installed first, and that detail matters, because the package never installs the bundler for you.

js
import WebpackBar from "webpackbar";

export default {
  entry: "./src/entry.js",
  plugins: [
    new WebpackBar({
      /* options */
    }),
  ],
};

On the next build you should see one bar. It carries the label `webpack`, because that is the default `name`, and it is drawn in `green`, because that is the default `color`. Nothing has to be switched on for that to happen, and nothing extra appears either: `profile` defaults to `false`, so the profiler stays hidden until you ask for it by name.

Reporters subscribe to eight lifecycle hooks

The plugin hands every reporter the same object, and a reporter only has to carry the hooks it cares about. A minimal custom logger looks like this:

js
const logger = {
  start(context) {
    // Called when (re)compile is started
  },
  change(context) {
    // Called when a file changed on watch mode
  },
  done(context) {
    // Called when compile finished
  },
};

`update`, `progress`, `allDone`, `beforeAllDone` and `afterAllDone` follow the same one argument shape, and every one of them is optional. That single argument is the plugin instance itself, referred to in the documentation as `context`, and its `state` property carries the running status:

js
{
  start, progress, message, details, request, hasErrors;
}

The split between the two is what makes a reporter cheap. The lifecycle tells you when something happened, and `context.state` tells you what was true at that moment, so a reporter never keeps its own copy of progress or error state. A reporter can be a class instance or a plain object holding functions, and the plugin calls only the ones it finds on it.

CI and minimal environments pick different reporters

Two defaults decide what reaches your terminal, and both read the environment before they decide. `fancy` is `true` when not in CI or testing mode, and it drives the bars reporter. `basic` is `true` when running in minimal environments, and it is a plain log reporter that prints only start and end. Override either when the automatic choice is wrong: a CI job that wants bars has to set `fancy` itself, and a terminal without ANSI support still receives the two line output through `basic`.

Custom output enters through two slots. `reporter` takes a single object, `reporters` takes an array and defaults to an empty one, and both sit next to the two reporters the package already ships. The documentation is blunt about combining them: set `fancy` and `basic` to false before supplying your own reporter, to prevent conflicts. What the repository never shows is what a conflict looks like on screen, or whether the profiler still draws once both built in reporters are switched off.

Concurrent builds collide on the default name

Concurrent builds are the case the `name` option exists for, and the README ties that case to SSR, where one server bundle and one client bundle can start together. Each instance draws its own bar, and the label on that bar is whatever `name` sets, which defaults to `webpack`. Two instances constructed with no options at all therefore both call themselves `webpack`, so a multi compiler setup has to name them by hand.

`color` has the same per instance shape, a single value defaulting to `green`, and it accepts a HEX form like `#xxyyzz` or a plain web colour name. Two builds in one process are separated by label first and colour second, and neither value is shared between instances unless you pass the same options object to both.

Rspack arrives through a second export path

Rspack is not a separate package. It is a second export path inside the same one, and the README gives it its own config file:

js
import WebpackBar from "webpackbar/rspack";

export default {
  entry: "./src/entry.js",
  plugins: [
    new WebpackBar({
      /* options */
    }),
  ],
};

Import from `webpackbar/rspack` and install `@rspack/core` as the peer dependency in place of `webpack`. Options and the reporter contract are shared with the webpack entry point, and the export map in `package.json` is what keeps the two apart:

json
"exports": {
  ".": {
    "import": { "types": "./dist/webpack.d.mts", "default": "./dist/webpack.mjs" },
    "require": { "types": "./dist/webpack.d.cts", "default": "./dist/webpack.cjs" }
  },
  "./rspack": {
    "import": { "types": "./dist/rspack.d.mts", "default": "./dist/rspack.mjs" },
    "require": { "types": "./dist/rspack.d.cts", "default": "./dist/rspack.cjs" }
  }
},
"main": "./dist/webpack.cjs",
"types": "./dist/webpack.d.cts"

`type` is `module`, so the legacy `main` and `types` fields point at the CommonJS pair for anything still reading those names instead of the export map.

The two descriptions disagree, which is the detail a skim loses. The repository is still summarised as an Elegant ProgressBar and Profiler for Webpack 3 , 4 and 5, odd space before the comma included, while the `description` field for 7.0.0 reads Elegant ProgressBar and Profiler for Webpack and Rspack. The rspack export path, the `@rspack/core` peer and the `play:rspack` script all ship in 7.0.0, so the summary line trails the package by an entire bundler.

A misspelled hook name never fires

The plugin calls only the functions it finds on a reporter. That makes the contract easy to satisfy and easy to break without noticing. An object with `start` and `done` is a valid reporter. So is an object whose hook name is misspelled, and a build carrying such a reporter finishes with nothing to say the hook you were waiting on never ran. The repository contains no warning for that case, and no list to check a name against beyond the object shown in the README.

The profiler is the second soft spot. `profile` defaults to `false`, and the only account of what it produces is a screenshot captioned Build Profiler. No field names, no timing breakdown, no statement of where the measurements come from. A team chasing profiling data has to judge that screen on its own before committing.

The state object is the third. `context.state` lists `start`, `progress`, `message`, `details`, `request` and `hasErrors`, but no type is attached to any of them and no example reads a single field. `hasErrors` could be a flag or a count, `request` could be a number or an object, and the README does not say which.

Hand-written reporters on the same compiler events

The alternative to this package is not another progress bar but a plugin you write yourself, and the reporter shape above works as a specification for one. React to a start, react to a change in watch mode, receive progress, react to a finish, then handle the moment every compilation is done, with `beforeAllDone` and `afterAllDone` bracketing that last step. A hand written plugin subscribes to the bundler lifecycle itself and keeps whatever state it needs.

The difference in approach is who owns that state. webpackbar builds one status object and passes it to every reporter as `context.state`, so a second output format costs one entry in `reporters` and no new bookkeeping. A hand written plugin owns the bookkeeping and receives no bar, no profiler and no per build labels for free.

Two things are worth checking either way. The README still defines a checks badge pointing at `nuxt-contrib/webpackbar`, while the `repository` field names `unjs/webpackbar`, so a link a fresh reader follows can land on the former organisation page. And the gate in front of publishing is `pnpm lint && vitest run`, so linting and the test suite decide what a maintainer is able to ship.

Editorial conclusion

Adopt webpackbar when a build runs long enough to stare at, when one process starts more than one compilation, or when you want output other than the packaged bars, and set `name` per instance while switching `fancy` and `basic` off as soon as you add your own reporter. Skip it when the profiler is your reason for looking, because nothing in the repository says what it records or where the numbers come from, and check every hook name against the reporter object before you trust a silent one. Read `context.state` in your own build first.

Frequently asked questions

How do I send webpackbar output to my own logger instead of the packaged bar?

Pass an object through `reporter`, or an array through `reporters`, and set `fancy` and `basic` to false so the built in reporters do not compete with it. The object can be a plain object or a class instance, and the plugin calls only the hook functions it finds there.

Which peer dependency does webpackbar need for webpack and for rspack?

For webpack builds the README asks you to make sure the `webpack` peer dependency is installed. For rspack builds you import from `webpackbar/rspack` and need `@rspack/core`. The package installs neither bundler itself, and ships only `dist` at install time.

What does the webpackbar profile option actually show?

It is a boolean that defaults to `false` and turns on a build profiler view. The only description available is a screenshot captioned Build Profiler, with no field names, no timing breakdown and no statement about where the measurements come from.

Why is my webpackbar progress bar missing on CI?

`fancy` defaults to true only when not in CI or in testing mode, while `basic` defaults to true only in minimal environments, and it is a plain log reporter limited to start and end. Set both yourself when the automatic choice does not match the log you want.

How does webpackbar tell two concurrent builds apart?

Each instance draws its own bar, labelled by the `name` option, which defaults to `webpack`, so instances built with no options share one label and have to be named by hand. The `color` option applies per instance too, defaulting to `green`, and accepts HEX or web colour names.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. unjs/webpackbar on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unjs-webpackbar.svg)](https://hysenlabs.com/projects/unjs-webpackbar)

Was this useful?