# BundleWatch > BundleWatch checks the size of built browser assets in CI. It fails the build when a file is over its maxSize, compares each file with the last sizes saved for the base branch, and reports the result as a GitHub commit status with a link to a results page. Install it with `npm install --save-dev bundlewatch`, create `bundlewatch.config.mjs` with a default-exported configuration object containing a `files` array, and run `npx bundlewatch` after the build. It searches for `bundlewatch.config.*` from the current directory up to the nearest `package.json`, checking config files before falling back to the package’s `bundlewatch` key. Use `--config` for a custom filename or location. It requires Node.js 22.17 or later. On GitHub Actions, pass the workflow's token as `BUNDLEWATCH_GITHUB_TOKEN` and grant `statuses: write`. --- Source: https://bundlewatch.io/guide/what-is-it # What is BundleWatch? BundleWatch checks the size of your built browser assets, so files don't grow without anyone noticing. You list your files and how big they may get. BundleWatch measures them after every build, fails the build when one is too big, and shows on each pull request how much every file changed. ## What it catches A sudden increase in size usually means something went wrong: - a new dependency that bloats your bundle - an accidental import of a whole library - a missing minification or compression step - slow growth that nobody notices, one pull request at a time ## How it works 1. After your build, BundleWatch measures each file the way browsers download it: compressed with gzip by default, or brotli, or uncompressed. 2. A file over its `maxSize`, or a path that matches no files, fails the run. The command exits with code 1, which fails your CI job. 3. On CI, BundleWatch fetches the sizes saved for the base branch, usually the one you're merging into, from the BundleWatch service. This _baseline_ shows how much each file changed. 4. The result appears as a `bundlewatch` status on the commit, with a link to a page that breaks it down file by file. 5. Builds of tracked branches, such as `main`, save their sizes as the new baseline that later pull requests are compared with. The service at `service.bundlewatch.io` is free. BundleWatch sends it file names and sizes, the repository, branch and commit, and your GitHub token so it can check your access. It never sends the files themselves. You can also [run your own](https://bundlewatch.io/guide/using-a-custom-server), or none at all: without a service, BundleWatch still checks `maxSize`. Anyone with a results link can open its page: see [Results links](https://bundlewatch.io/guide/using-bundlewatch#results-links). ## Blocking merges To stop pull requests that make a file too big from being merged, make `bundlewatch` a required status check in your repository's branch protection rules or rulesets. See [GitHub statuses](https://bundlewatch.io/guide/using-bundlewatch#github-statuses). --- Next: [Getting started](https://bundlewatch.io/guide/getting-started), or [set it up with a coding agent](https://bundlewatch.io/guide/getting-started#set-up-with-an-ai-agent). --- Source: https://bundlewatch.io/guide/getting-started # Getting started BundleWatch needs Node.js 22.17 or later. Upgrading from 0.x: see [Migrating to 1.0](https://bundlewatch.io/guide/migrating-to-v1). To let a coding agent do the setup, copy [this prompt](https://bundlewatch.io/guide/getting-started#set-up-with-an-ai-agent). ## Installation Install BundleWatch as a dev dependency: npm: ```bash npm install bundlewatch --save-dev ``` pnpm: ```bash pnpm add bundlewatch --save-dev ``` Yarn: ```bash yarn add bundlewatch --dev ``` Bun: ```bash bun add bundlewatch --dev ``` ## Configuration Create `bundlewatch.config.mjs` in your project root to tell BundleWatch which files to check, and how big they may get: bundlewatch.config.mjs: ```js /** @type {import('bundlewatch').BundlewatchConfig} */ export default { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], } ``` Build your project, then run: ```bash npx bundlewatch ``` BundleWatch finds `bundlewatch.config.mjs` automatically. Use `--config ` for a custom filename or location; see [config discovery](https://bundlewatch.io/guide/using-a-config-file#config-discovery). The first run on your machine prints something like this: ```text [WARNING] Some CI configuration options were not found (githubAccessToken, repoOwner, repoName, commitSha): bundlewatch will be unable to report build status, or save comparison data Learn more at: https://bundlewatch.io/ Result breakdown at: https://service.bundlewatch.io/results?d=... PASS ./dist/app.js: 30.71KB < 100KB (gzip) bundlewatch PASS Everything is in check (+30.71KB, -0B) ``` The warning is expected on your machine: in CI, you provide the token, and BundleWatch detects the other values on [supported providers](https://bundlewatch.io/guide/using-bundlewatch#supported-ci-providers). The command exits with code 1 when a file is over its `maxSize`, or when a path matches no files. To choose limits, run `npx bundlewatch` before adding `maxSize`. A file without one has no limit, and shows as `PASS ./dist/app.js: 30.71KB < Infinity (gzip)`. Then set each `maxSize` a little above the current size, so the check catches real increases without failing on every small change. If your file names contain content hashes, such as `main.3f2a1b9c.js`, set [`normalizeFilenames`](https://bundlewatch.io/reference/configuration#normalizefilenames). Otherwise each build's files look new, and there is nothing to compare them with. See [Using a config file](https://bundlewatch.io/guide/using-a-config-file) for other ways to configure BundleWatch, and the [configuration reference](https://bundlewatch.io/reference/configuration) for every option. ## Running in CI Run BundleWatch in CI after your build, so that it posts a status on each pull request and compares every file with the base branch: - On GitHub Actions, copy [this workflow](https://bundlewatch.io/guide/using-bundlewatch#github-actions) to `.github/workflows/bundlewatch.yml`. It uses the token GitHub creates for each run, so you don't need to create one. - On another CI provider, [create a token](https://bundlewatch.io/guide/using-bundlewatch#other-ci-providers), store it as the secret `BUNDLEWATCH_GITHUB_TOKEN`, and run `npx bundlewatch` after the build. Pull requests have nothing to compare with until a push to a [tracked branch](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#tracked-branches), such as `main`, saves a baseline. Until then the log shows `Unable to fetch fileDetails … code=404`. ## Set up with an AI agent Copy this prompt into your coding agent. It reads this documentation, measures your build, picks limits and adds the CI setup: ```text Set up BundleWatch (https://bundlewatch.io) in this repository, so CI fails when a built file gets too big, and pull requests show how much each file changed. Read https://bundlewatch.io/llms-full.txt first: it has the whole documentation. If this repository already uses BundleWatch 0.x, follow https://bundlewatch.io/guide/migrating-to-v1.md instead. Then: 1. Find the production build command and run it. List the JavaScript and CSS files it outputs, with their gzip sizes. 2. Add bundlewatch as a dev dependency, with the package manager this project already uses. 3. Create bundlewatch.config.mjs in the project root with a default-exported configuration object and one "files" entry per output. Use globs that match only files, not directories, and still match when file names change between builds, and a "maxSize" about 10% above each file's current gzip size, rounded up to a whole kB. If file names contain content hashes, set "normalizeFilenames" so that a file keeps the same name across builds. Check the pattern against the actual file names: it must remove only the hash, and no two files may end up with the same name. 4. Build, run `npx bundlewatch`, and make sure every file passes. 5. Run it in CI after the build: - On GitHub Actions, add the workflow from https://bundlewatch.io/guide/using-bundlewatch#github-actions, adapted to this project's Node.js version, package manager, build command and default branch. It needs no secret. - On another CI provider, add a step that runs `npx bundlewatch` after the build, and ask me to create a token at https://service.bundlewatch.io/setup-github and store it as the BUNDLEWATCH_GITHUB_TOKEN secret. Never commit a token. 6. Summarize what you changed, the limits you chose, and what is left for me to do, such as making `bundlewatch` a required status check. ``` The whole documentation is also available as plain text for agents and LLMs: [`llms.txt`](https://bundlewatch.io/llms.txt) lists the pages, and [`llms-full.txt`](https://bundlewatch.io/llms-full.txt) contains all of them. Every page is available as Markdown too, by adding `.md` to its URL. ## Next steps - [GitHub statuses](https://bundlewatch.io/guide/using-bundlewatch#github-statuses): block merges when a file is too big. - [Base and tracked branches](https://bundlewatch.io/guide/diffing-against-branches-other-than-master): what each build is compared with. - [Configuration reference](https://bundlewatch.io/reference/configuration): every option and environment variable. - [Troubleshooting](https://bundlewatch.io/guide/troubleshooting): what each log message means. --- Source: https://bundlewatch.io/guide/using-bundlewatch # Running in CI Run BundleWatch in CI after your build. With a GitHub token, it posts a `bundlewatch` status on the commit and compares each file with the [base branch](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#base-branch). Your repository has to be on GitHub; the CI can be anywhere. The examples below use `bundlewatch.config.mjs` from [Getting started](https://bundlewatch.io/guide/getting-started#configuration). BundleWatch discovers it automatically. For a custom filename or a file outside the search path, add `--config ` to the command. ## GitHub Actions On GitHub Actions, BundleWatch can use the token GitHub creates for every workflow run, so there is no token to generate or store. Add a workflow such as `.github/workflows/bundlewatch.yml`: ```yaml name: bundlewatch on: push: branches: [main] # the branches you track pull_request: permissions: contents: read statuses: write jobs: bundlewatch: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npm run build - run: npx bundlewatch env: BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` - `statuses: write` lets BundleWatch post the `bundlewatch` commit status with the workflow's token. Keep `contents: read` as well: a `permissions` block removes every permission it doesn't list. - The service accepts the workflow's token for saving baselines and creating short results links. - The `push` trigger lists only the branches you track. Otherwise a push to a branch with an open pull request would run the workflow twice, and both runs would post the same status on one commit. - The repository, commit, branch and pull request target are detected automatically, including the repository's default branch. On pull requests, the status goes on the pull request's head commit. - Pushes to a [tracked branch](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#tracked-branches) save their results as the new baseline. Pull requests are compared with the baseline of the branch they target. > **Pull requests from forks** > > GitHub gives pull requests from forks a read-only token, so BundleWatch can't post a status on them, and logs a `403` error from GitHub instead. The job still fails when a file is over its `maxSize`. ## Other CI providers 1. Open [service.bundlewatch.io/setup-github](https://service.bundlewatch.io/setup-github), signed in to GitHub as a user with push access to the repository, and authorize BundleWatch. The token has only the `repo:status` scope: it can read and write commit statuses, not your code. 2. In your CI settings, store the token as a secret environment variable named `BUNDLEWATCH_GITHUB_TOKEN`. Never commit it. 3. Run `npx bundlewatch` after your build step. 4. If your provider isn't in the table below, or doesn't supply a value, set [these variables](https://bundlewatch.io/guide/using-bundlewatch#required-ci-environment-variables). Without a token, BundleWatch only checks `maxSize`. ### Supported CI providers BundleWatch reads the repository, commit and branch from these providers' own environment variables. Pull request builds are compared with a baseline but never save one ([details](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#pull-request-builds)). | Provider | Base branch of pull requests | Counts as a pull request build | | ------------------------------------------------------ | ------------------------------------------------------------- | ----------------------------------------------- | | GitHub Actions | Detected. Other builds use the repository's default branch | `pull_request` and `pull_request_target` events | | GitLab CI | Detected in merge request and external pull request pipelines | Those pipelines | | Travis CI | Detected | Pull request builds | | Drone | Detected | `pull_request` events | | Buddy, Codefresh | Detected | Not detected | | CircleCI | Not available | Builds of pull requests from forks | | Netlify | Not available | Deploy previews | | Cloudflare Pages, Codeship, Wercker | Not available | Not detected | | ZEIT Now (the legacy `NOW_GITHUB_*` variables) | Not available | Not detected | | Generic `CI_*` variables (`CI` set, no provider above) | `CI_TARGET_BRANCH` | `CI_EVENT=pull_request` | Where the base branch isn't available, builds are compared with `master` unless you set `CI_BRANCH_BASE`. BundleWatch doesn't read Vercel's `VERCEL_*` variables, and some providers above don't supply the repository: set the [variables below](https://bundlewatch.io/guide/using-bundlewatch#required-ci-environment-variables) for anything that isn't detected. ### Variables to set when they aren't detected Set these for any value your CI provider doesn't supply: - `CI_REPO_OWNER` (e.g. `facebook` in github.com/facebook/react) - `CI_REPO_NAME` (e.g. `react` in github.com/facebook/react) - `CI_COMMIT_SHA` - `CI_BRANCH` - `CI_BRANCH_BASE`, the branch to compare with, if it isn't `master` See the [environment variables reference](https://bundlewatch.io/reference/configuration#environment-variables) for every variable BundleWatch reads. ## GitHub statuses BundleWatch reports on the commit it checks, as GitHub commit statuses: ![build status preview](https://bundlewatch.io/build-status-preview.png) | Status | State | Description | | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `bundlewatch` | pending | `Checking bundlewatch...` while it runs | | `bundlewatch` | success | The summary, such as `Everything is in check (+1.2KB, -0B, +0.4%)`: the bytes files gained and lost compared with the baseline, and the change in total size. Without a baseline, every file counts as gained and there's no percentage. | | `bundlewatch` | failure | `maxSize check failed` | | `bundlewatch` | error | `Unable to analyze, check logs.` and the error, after an unexpected error. | | `bundlewatch ` | failure | One per failing file, with its result, such as `198.6KB > 100KB (gzip)`. There's no description when the path matched no files or couldn't be read. | - When there is a [results link](https://bundlewatch.io/guide/using-bundlewatch#results-links), the success or failure `bundlewatch` status links to it. The other statuses have no link. - If GitHub can't be reached, a status keeps the last state GitHub accepted, such as pending: see [Troubleshooting](https://bundlewatch.io/guide/troubleshooting#github-statuses). - Paths longer than 35 characters show as `*` followed by their last 37 characters, with ` (2)` added when two paths end the same way. - Descriptions longer than 140 characters are cut. - At most five statuses per run: `bundlewatch` and four files. The other failing files are only listed in the log and on the results page. - An invalid configuration is reported before any status is posted. Statuses need a token, the repository and the commit. Without them, BundleWatch only prints its results and sets the exit code. To block pull requests that make a file too big, make `bundlewatch` a required status check, in a branch protection rule or a ruleset of your repository. Require only `bundlewatch`: the per-file statuses appear only when a file fails. ## Results links Each run prints a link to its results page (`Result breakdown at: …`) and uses it as the _details_ link of the `bundlewatch` status. - When the service accepts your token for writing, the link is short, for example `https://service.bundlewatch.io/r/3fK9xQ2LmP7a`. It accepts a token whose user has push access to the repository, or the workflow's `GITHUB_TOKEN` on GitHub Actions. The service stores the results and renders the page from that copy. - Otherwise the results are packed into the link itself: `https://service.bundlewatch.io/results?d=…`. That happens without a token, with a token the service doesn't accept for writing, with a service older than 1.0, and when creating the short link fails or takes more than 5 seconds. For large projects these links can get too long for some browsers and servers. - With `bundlewatchServiceHost: null`, no link is printed and the GitHub status has no _details_ link. Results pages don't check access: anyone with a results link can open its page, even for a private repository. A page shows the repository, branch and commit, and each file's name, size and limit. It never contains your token or your files. ## Results page The results link opens a page that starts with what the commit changed compared with the base branch: the bytes it adds and removes, and how much larger or smaller the files are in total. ![build results preview](https://bundlewatch.io/build-results-preview.png) - Files are listed by how much they changed, largest first, each next to its limit. A file at 90% of its limit or more is marked. Files that didn't change are collapsed. - Until the base branch has a baseline, files are listed by size against their limit instead. - Paths that matched no files, or couldn't be read, are listed at the top with the reason. - "All files" lists every file in a table: its size, its size on the base branch, the change and its limit. A short link that doesn't exist or has expired shows "Results not found". A self-hosted service keeps short links for [`ResultsRetentionDays`](https://bundlewatch.io/guide/using-a-custom-server#deploying-the-service) days, or forever by default. If something doesn't work, look up the log message in [Troubleshooting](https://bundlewatch.io/guide/troubleshooting). --- Source: https://bundlewatch.io/guide/using-a-config-file # Using a config file Create `bundlewatch.config.mjs` in your project root, then run: ```bash npx bundlewatch ``` A standalone config file is the recommended setup. BundleWatch discovers the filenames shown below automatically; see [config discovery](https://bundlewatch.io/guide/using-a-config-file#config-discovery) for the search order. bundlewatch.config.mjs: ```js /** @type {import('bundlewatch').BundlewatchConfig} */ export default { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], } ``` bundlewatch.config.mts: ```ts import type { BundlewatchConfig } from 'bundlewatch' export default { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], } satisfies BundlewatchConfig ``` bundlewatch.config.cjs: ```js module.exports = { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], } ``` bundlewatch.config.json: ```json { "files": [ { "path": "./dist/*.js", "maxSize": "100kB" } ] } ``` - Config files can be `.json`, `.js`, `.cjs` or `.mjs`, or `.ts`, `.mts` or `.cts` on Node.js 22.18 and later, which run TypeScript natively. `--config` paths are resolved from the current directory; absolute paths work too. - The `BundlewatchConfig` type gives your editor completion and checks for every option. - In TypeScript, prefer `.mts`: Node.js loads it as an ES module in any package. A `.ts` file works too, but in a package without `"type": "module"`, Node.js prints a warning before loading it. - A JavaScript or TypeScript config can also have only named exports, such as `export const files = [...]`, instead of a default export. - A JavaScript config can set options that JSON can't, such as a `RegExp` for [`normalizeFilenames`](https://bundlewatch.io/reference/configuration#normalizefilenames), or values computed from environment variables. Your configuration is merged with the [defaults](https://bundlewatch.io/reference/configuration). If more than one source is present, see [Where configuration comes from](https://bundlewatch.io/reference/cli#where-configuration-comes-from). You can also supply the config directly to the [Node.js API](https://bundlewatch.io/reference/nodejs). ## Config discovery Without command-line globs or `--config`, BundleWatch searches from the current directory upward. In each directory it checks these filenames, in order: 1. `bundlewatch.config.mjs` 2. `bundlewatch.config.js` 3. `bundlewatch.config.cjs` 4. `bundlewatch.config.mts` 5. `bundlewatch.config.ts` 6. `bundlewatch.config.cts` 7. `bundlewatch.config.json` The first file found wins. The search includes the directory containing the nearest `package.json`, then stops. If no config file is found, BundleWatch reads that package's `bundlewatch` key. It does not search beyond that package, even if the key is missing. Without a `package.json`, it searches up to the filesystem root. A file that cannot be loaded, or whose configuration is invalid, fails the run; BundleWatch does not fall back to another file or to `package.json`. File globs remain relative to the directory where you run BundleWatch, even when the config is in a parent directory. For a custom filename or location, use `--config`. This overrides discovery and can select a shared config outside the current package: ```bash npx bundlewatch --config ../shared/bundlewatch.config.mjs ``` ## Alternative: package.json You can also keep configuration under the `bundlewatch` key in `package.json`: package.json: ```json { "bundlewatch": { "files": [ { "path": "./dist/*.js", "maxSize": "100kB" } ] } } ``` Run `npx bundlewatch` to read this configuration when no config file is discovered. Command-line globs, `--config`, and discovered config files take precedence over this key. In a monorepo, keep the key in the package’s own `package.json`, or use `--config` to select a shared file explicitly. --- Source: https://bundlewatch.io/guide/diffing-against-branches-other-than-master # Base and tracked branches BundleWatch compares each build with a baseline: the last results saved for the base branch. Two settings control this: - The base branch (`ci.repoBranchBase`) is the branch the current build is compared with. - The tracked branches (`ci.trackBranches`) are the branches whose builds save a new baseline. ## Base branch BundleWatch uses the first of these that is available: 1. `ci.repoBranchBase` in your [configuration](https://bundlewatch.io/reference/configuration#ci). 2. The `CI_BRANCH_BASE` environment variable. 3. The pull request's target branch, if your CI provider supplies it ([by provider](https://bundlewatch.io/guide/using-bundlewatch#supported-ci-providers)). 4. On GitHub Actions, the repository's default branch. 5. `master`. On GitHub Actions this means pull requests are compared with the branch they target, and other builds with your default branch, without any configuration. When none of the first four is available, as for pushes on GitLab CI or Travis CI and every build on CircleCI, the build is compared with `master`. If your main branch has another name, or you want to compare with a branch such as `develop`, set `CI_BRANCH_BASE`. It applies to every build, including pull requests that target another branch. ## How files are matched Each file is compared with the baseline entry of the same name: its path as your glob spells it, after `normalizeFilenames`. `./dist/app.js` and `dist/app.js` are different names, so changing `./dist/*.js` to `dist/*.js` shows every file as removed and new. Sizes are compared without checking their compression, so switching a file from gzip to brotli shows as a change in size. These differences last until a tracked-branch build with the new configuration saves its baseline. ## Tracked branches By default, BundleWatch tracks `master`, `main` and `develop`, plus the repository's default branch when it can be detected (on GitHub Actions). A build saves its results as the baseline for its branch when: - its branch is tracked, - it is not a pull request build, and - every file was found and could be read. A build that fails its `maxSize` check still saves its sizes. To track other branches, such as `staging`, add the `trackBranches` option to `ci` in `bundlewatch.config.mjs`. The list replaces the default one, so include every branch you want to track: bundlewatch.config.mjs: ```js export default { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], ci: { trackBranches: ['main', 'develop', 'staging'], }, } ``` Run with this configuration: ```bash npx bundlewatch ``` > **One baseline per branch** > > Each branch has one baseline. If several BundleWatch runs save on the same branch, such as one per package in a monorepo, the last one replaces the others, and later comparisons show the other runs' files as removed. Check all your files in one run. ### Pull request builds Pull request builds are compared with the baseline but never save one, even when the pull request's branch has a tracked name, such as a fork's `master`. Only builds that the CI provider runs specifically for a pull request count: see [Supported CI providers](https://bundlewatch.io/guide/using-bundlewatch#supported-ci-providers). Branch builds keep saving, even when the branch has an open pull request. Set `ci.isPullRequest` in a [JavaScript config file](https://bundlewatch.io/guide/using-a-config-file) to override the detection. --- Source: https://bundlewatch.io/guide/using-a-custom-server # Using a custom server The BundleWatch service stores the baselines your builds are compared with, creates short results links and renders the results pages. You can run your own instance instead of using `https://service.bundlewatch.io`. Point BundleWatch at your service in `bundlewatch.config.mjs`: bundlewatch.config.mjs: ```js export default { files: [ { path: './dist/*.js', maxSize: '100kB', }, ], bundlewatchServiceHost: 'https://bundlewatch.example.com', } ``` Run with this configuration: ```bash npx bundlewatch ``` See [bundlewatchServiceHost](https://bundlewatch.io/reference/configuration#bundlewatchservicehost) for the URL format, and to run without a service. ## Deploying the service The service lives in the BundleWatch repository, in [`packages/service`](https://github.com/bundlewatch/bundlewatch/tree/main/packages/service). It runs on AWS Lambda, stores its data in DynamoDB, and is deployed with AWS SAM from [`template.yaml`](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/template.yaml). 1. Deploy [`infra/bootstrap.yaml`](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/infra/bootstrap.yaml) in your AWS account and region, as described in [Running your own service](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/README.md#running-your-own-service). Set `GitHubRepository` to your fork, and give it the same `StoreTableName` and `ResultsTableName` that you give template.yaml in step 3. Set `ServiceStackName` to your application stack's name. If your account already has a GitHub OIDC provider, pass its ARN as `ExistingOidcProviderArn`. 2. Create a GitHub OAuth app for `/setup-github`, with the callback URL `/setup-github`. 3. Deploy [`template.yaml`](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/template.yaml) with your service's URL, OAuth app and table names, as described in [Running your own service](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/README.md#running-your-own-service). `template.yaml` also has these two parameters: - `WriteAuthorization`: `enforce` to apply the stricter check for saving baselines and creating short links from the start. The default, `report`, authorizes like 0.x and only logs where the stricter check would decide differently. See [Service changes](https://bundlewatch.io/guide/migrating-to-v1#service-changes). - `ResultsRetentionDays`: how many days to keep short links. The default, `0`, keeps them forever. ## GitHub Enterprise - Commit statuses go to the GitHub API at `GITHUB_API_URL` when it is set; GitHub Actions on GitHub Enterprise Server sets it automatically. - service.bundlewatch.io only accepts github.com tokens. On GitHub Enterprise it can't compare with baselines, save them or create short links: each run logs `401` errors and prints a long results link. - For comparisons, deploy your own service with `GitHubApiUrl` set to your API URL, such as `https://github.example.com/api/v3`, and set `bundlewatchServiceHost` to its URL. - Otherwise set `bundlewatchServiceHost: null`, so that your token and file names aren't sent to service.bundlewatch.io. ## Services from before 1.0 A service forked from the old `bundlewatch/service` repository keeps working with the 1.0 CLI. It doesn't support short links, so the CLI prints long `/results?d=…` links instead. See [Migrating to 1.0](https://bundlewatch.io/guide/migrating-to-v1#service-changes) for what changed in the service. --- Source: https://bundlewatch.io/guide/troubleshooting # Troubleshooting Find the message from your log below. Set `DEBUG` to any non-empty value to log the details of failed requests to the BundleWatch service. ## Configuration | Message | What it means and what to do | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `[ERROR] Configuration missing` | The selected configuration has no files to check. Without globs or `--config`, BundleWatch searches for `bundlewatch.config.*` up to and including the nearest `package.json` directory, then falls back to its `bundlewatch` key. Add `files` to that configuration, or select a file with `--config`. See [config discovery](https://bundlewatch.io/guide/using-a-config-file#config-discovery). Exits with code 1. | | `[FATAL] config.files must be an Array` or `[FATAL] config.files must only contain objects with a path, e.g. { path: "./dist/*.js" }` | `files` has the wrong shape; see [files](https://bundlewatch.io/reference/configuration#files). Exits with code 1. | | `[FATAL] config.compression must be a valid type` | `defaultCompression` or `--compression` isn't `gzip`, `brotli` or `none`, or `--compression` has no value. Exits with code 1. | | `[FATAL] config.ci.trackBranches must be an Array` | Write it as a list, such as `["main"]`. Exits with code 1. | | `[FATAL] config.normalizeFilenames (…) is not a valid RegExp.` | The string isn't a valid regular expression, or `--normalize` has no value. In JSON, double each backslash. Exits with code 1. | | `[FATAL] Could not find JSON config file: …`, `[FATAL] Could not parse JSON config file: …`, `[FATAL] Exception while loading JS config: …` or `[FATAL] Could not parse …/package.json` | The file couldn't be read or loaded. Just above, the log shows the error and one of `[ERROR] Exception while parsing package.json`, `[ERROR] Exception while loading JS config`, `[ERROR] Exception while trying to read JSON config file` or `[ERROR] Exception while parsing JSON config`. `--config` paths are relative to the current directory. Exits with code 1. | | `[FATAL] --config needs the path of a config file` | `--config` has no value, or the next argument starts with `-`. Exits with code 1. | | `[ERROR] unknown option '…'` | Check the spelling against [Options](https://bundlewatch.io/reference/cli#options). Exits with code 1. | | `[WARNING] CLI files supplied, config in package.json will be ignored` and the other `… will be ignored` warnings | More than one configuration source is present; see [Where configuration comes from](https://bundlewatch.io/reference/cli#where-configuration-comes-from). The run continues with the first source. | ## Files | Message | What it means and what to do | | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `[ERROR] There is no matching file for ./dist/*.js` | The path matched no files, which fails the run. Check that the build ran first, and that paths are relative to the directory you run BundleWatch in. | | `[ERROR] Could not read file ` | The glob matched a directory, or a file BundleWatch can't read. This fails the run. Match files only, such as `./dist/**/*.js` instead of `./dist/*`. | | `[INFO] Some files could not be measured, no results saved` | Because of the file errors above, this build doesn't save a baseline, and the run fails. Fix those errors. | | `PASS : : File removed (… smaller than ) (gzip)` | The file is in the baseline but not in this build. It doesn't fail the check, and it disappears once a tracked-branch build saves a baseline without it. If you didn't remove the file, check that your globs still spell its path the same way and that `normalizeFilenames` still gives it the same name: see [How files are matched](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#how-files-are-matched). | | `< Infinity`, as in `PASS ./dist/app.js: 30.71KB < Infinity (gzip)` | The file has no `maxSize`, so it can't fail; see [files](https://bundlewatch.io/reference/configuration#files). | ## Baselines and comparisons | Message | What it means and what to do | | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `[WARNING] Some CI configuration options were not found (…)` | BundleWatch is missing the listed values, so it only checks `maxSize`. This is expected when you run it on your machine. In CI, set the token or the [variables](https://bundlewatch.io/guide/using-bundlewatch#required-ci-environment-variables) it lists. | | `[WARNING] bundlewatchServiceHost was not supplied, bundlewatch comparisons unavailable` | Expected with `bundlewatchServiceHost: null`. The run continues without a comparison. | | `[WARNING] The ci.repoBranchBase was not supplied, bundlewatch comparisons unavailable` | Your configuration sets `ci.repoBranchBase` to an empty value, such as `''` or `null`, which replaces the detected base branch and `CI_BRANCH_BASE`. Remove it, or set it to a branch name. The run continues without a comparison. | | `[WARNING] The ci.repoCurrentBranch was not supplied, bundlewatch results will not be saved` | BundleWatch doesn't know which branch is being built, so the run compares with the baseline but doesn't save one. Set `CI_BRANCH`, and remove any empty `ci.repoCurrentBranch` value from your configuration: it replaces `CI_BRANCH`. | | `[ERROR] Unable to fetch fileDetails for baseBranch=… code=404` | The run continues without a comparison. No baseline has been saved for the base branch yet, for example on the first run. It goes away once a build of the base branch has saved one. If it doesn't, check that the base branch is right (see [Base and tracked branches](https://bundlewatch.io/guide/diffing-against-branches-other-than-master)) and that it is a tracked branch. | | The same message with `code=401` | The service refused the token for this repository, or the commit SHA is missing. On GitHub Enterprise, see [GitHub Enterprise](https://bundlewatch.io/guide/using-a-custom-server#github-enterprise). The run continues without a comparison. | | The same message with `code=503` | The service couldn't reach GitHub to check the token. The run continues without a comparison; rerun the job. | | The same message with `code=ECONNABORTED` | The service didn't answer within 10 seconds. Connection errors, such as `ENOTFOUND` or `ECONNREFUSED`, mean BundleWatch couldn't connect to it, for example behind a proxy (see [Behind a proxy](https://bundlewatch.io/guide/troubleshooting#behind-a-proxy)). The run continues without a comparison. | | `code=ERR_INVALID_URL` in any `Unable to …` message | `bundlewatchServiceHost` isn't a full URL, for example because it has no `https://`. See [bundlewatchServiceHost](https://bundlewatch.io/reference/configuration#bundlewatchservicehost). The run continues without the service. | | `[INFO] is not a branch to track, no results saved` | Expected on branches that aren't [tracked](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#tracked-branches): the run continues without saving a baseline. To save a branch's results, add it to `ci.trackBranches`. | | `[INFO] Pull request build, no results saved` | Expected: the run continues without saving a baseline. See [Pull request builds](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#pull-request-builds). | | `[ERROR] Unable to save fileDetails for currentBranch=… code=…` | The baseline wasn't saved, and the run continues. `401`: the service refused the token; on GitHub Actions, use the workflow's `GITHUB_TOKEN`, and elsewhere, use a token of a user with push access (see [Service changes](https://bundlewatch.io/guide/migrating-to-v1#service-changes)). `413`: the baseline is larger than 350 KB as JSON; `503` and network codes: as above. | | `[WARNING] Unable to create a short results link code=…` | The service failed, or didn't answer within 5 seconds, so the run continues and prints the long results link instead. If the service refuses the request, as a service older than 1.0 does, you get the long link without this warning. | ## GitHub statuses | Message | What it means and what to do | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `[ERROR] GitHubService HTTP_403 :: …` on pull requests from forks | Their token can't post statuses. The run continues; see [Pull requests from forks](https://bundlewatch.io/guide/using-bundlewatch#github-actions). | | Any other `[ERROR] GitHubService HTTP_ :: ` | GitHub refused the status, and the message is GitHub's. The run continues. On GitHub Actions, check that the workflow grants `statuses: write`. | | `[WARNING] Max reported statuses reached, github status will not be reported` | More than four files failed. The run continues; see [GitHub statuses](https://bundlewatch.io/guide/using-bundlewatch#github-statuses). | | `[FATAL] Uncaught exception`, after `Error: ECONNABORTED`, `ENOTFOUND`, `ECONNREFUSED` or a similar code | BundleWatch couldn't post a GitHub status: either GitHub didn't answer within 5 seconds (`ECONNABORTED`), or the connection failed. BundleWatch posts statuses whenever the token, the repository and the commit are known, so the run fails with exit code 1. For proxies, see [Behind a proxy](https://bundlewatch.io/guide/troubleshooting#behind-a-proxy). | ## Behind a proxy BundleWatch doesn't read proxy variables such as `HTTPS_PROXY`. Behind a proxy, allow direct connections to `api.github.com` (or your `GITHUB_API_URL`) and to the BundleWatch service, or unset the token to run only the size check. ## Creating a token | Message | What it means and what to do | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `That authorization expired` | No token was created: the authorization took more than 10 minutes, or started in another browser. Start again at [/setup-github](https://service.bundlewatch.io/setup-github), and finish in the same browser within 10 minutes, with cookies enabled. | | `GitHub didn’t issue a token` | GitHub didn't exchange the authorization for a token. The page shows GitHub's reason. Start again at [/setup-github](https://service.bundlewatch.io/setup-github). | --- Source: https://bundlewatch.io/guide/migrating-to-v1 # Migrating to 1.0 Most setups need no changes, but automatically discovered config files can now take precedence over `package.json`. Review [Config files](https://bundlewatch.io/guide/migrating-to-v1#config-files) before upgrading. CLI flags, environment variables, GitHub status names and exit codes remain supported. This page lists everything that behaves differently. [CONTRACT.md](https://github.com/bundlewatch/bundlewatch/blob/main/CONTRACT.md) lists what stays compatible. 1.0 is a new major version, so `^0.x` and `~0.x` ranges don't pick it up: nothing changes until you upgrade. Most of what follows applies equally if you're coming from 0.3.x. ## Upgrade by hand 1. Use Node.js 22.17 or later, in CI too. Node.js 16, 18 and 20 are end-of-life. 2. Update BundleWatch: npm: ```bash npm install bundlewatch@1 --save-dev ``` pnpm: ```bash pnpm add bundlewatch@1 --save-dev ``` Yarn: ```bash yarn add bundlewatch@1 --dev ``` Bun: ```bash bun add bundlewatch@1 --dev ``` 3. Go through [Things to check](https://bundlewatch.io/guide/migrating-to-v1#things-to-check) below. 4. Build, then run `npx bundlewatch`, keeping any existing globs or `--config ` arguments. Verify that it checks the intended files and limits. ## Upgrade with an AI agent Copy this prompt into your coding agent to upgrade a project: ```text Upgrade BundleWatch to 1.0 in this repository. Read https://bundlewatch.io/guide/migrating-to-v1.md for what changed, and https://bundlewatch.io/llms-full.txt for the whole documentation. Then: 1. Update BundleWatch to the latest 1.x version, with the package manager this project uses. It needs Node.js 22.17 or later: update the Node.js version used in CI if it's older. 2. Check the configuration against "Things to check" in the migration guide, and follow its links. Look for bundlewatch.config.* files that now take precedence over the package.json bundlewatch key, and preserve the files and limits checked by each existing command. A numeric "maxSize" is now enforced: if a file now fails a limit that was never checked before, don't raise the limit, tell me. 3. On GitHub Actions, switch to the workflow's GITHUB_TOKEN as described in the guide, and remove workarounds that 1.0 makes unnecessary, such as CI_BRANCH_BASE set only to avoid comparing with master, or CI_COMMIT_SHA set to the pull request's head commit. 4. Remove the brotli-size dependency if nothing else uses it. 5. Build, then run `npx bundlewatch` with any existing globs or `--config ` arguments, and verify that it checks the intended files and limits. Summarize what you changed, what now behaves differently, and what is left for me to do, such as deleting a token secret that is no longer used. ``` ## Things to check - A discovered `bundlewatch.config.*` file now takes precedence over the `bundlewatch` key in `package.json`. Check which files and limits each existing command should use. If a config file is only for a separate `--config` command, rename it outside the discovery list and update that command so a bare run keeps using `package.json`. See [Config files](https://bundlewatch.io/guide/migrating-to-v1#config-files). - A numeric `maxSize`, such as `102400`, is now enforced. If a file now fails, it was over its limit before too: see [Numeric maxSize](https://bundlewatch.io/guide/migrating-to-v1#numeric-maxsize). - `files` and `ci.trackBranches` replace the defaults instead of merging with them: see [Tracked branches](https://bundlewatch.io/guide/migrating-to-v1#tracked-branches). - On GitHub Actions, pushes are compared with the default branch instead of `master`: see [Base branch on GitHub Actions](https://bundlewatch.io/guide/migrating-to-v1#default-branch-detection-on-github-actions). - Pull request builds no longer save baselines: see [Pull request builds](https://bundlewatch.io/guide/migrating-to-v1#pull-request-builds-no-longer-save-baselines). - Tokens of users without push access will stop saving baselines: see [Service changes](https://bundlewatch.io/guide/migrating-to-v1#service-changes). - Deep imports into `bundlewatch/lib/…` no longer work, except `bundlewatch/lib/app` and `bundlewatch/lib/app/index.js`: see [Loading the package](https://bundlewatch.io/guide/migrating-to-v1#loading-the-package). - Behind a proxy, see [Proxy variables](https://bundlewatch.io/guide/migrating-to-v1#proxy-variables). On GitHub Enterprise, see [GitHub Enterprise](https://bundlewatch.io/guide/migrating-to-v1#github-enterprise). ## What changed ### Loading the package - `import bundlewatch, { STATUSES } from 'bundlewatch'` and `require('bundlewatch').default` / `.STATUSES` work as before. - The package ships both ES module and CommonJS builds, with TypeScript types. - `bundlewatch/lib/app` and `bundlewatch/lib/app/index.js` still load the main export, with `import` or `require()`, and running `node_modules/bundlewatch/lib/bin/index.js` directly still runs the CLI. - Other deep imports into `bundlewatch/lib/…` don't; use the package's main export. ### Short results links - Previously the results were packed into the URL and shortened with Firebase Dynamic Links, a product Google has shut down. Large projects produced URLs that browsers and servers refused (issues [#404](https://github.com/bundlewatch/bundlewatch/issues/404) and [#456](https://github.com/bundlewatch/bundlewatch/issues/456)). - Now, when the service accepts your token for writing, the link is short: `https://service.bundlewatch.io/r/`. See [Results links](https://bundlewatch.io/guide/using-bundlewatch#results-links) for when it isn't. - Each run with a token makes one more request to the service, to create the short link. It gives up after 5 seconds and falls back to the long link. - Existing `/results?d=…` links keep working, including those with an unlimited `maxSize`, which never rendered before. Old `ja2r7.app.goo.gl` short links work for as long as Google keeps redirecting them. ### `bundlewatchServiceHost: null` With the service disabled, no results link is printed, the GitHub status has no details link, and the [Node.js API](https://bundlewatch.io/reference/nodejs#result) returns `url: undefined`. Before 1.0 the link pointed to `null/results?d=…`. ### Service URLs with credentials Basic auth credentials in `bundlewatchServiceHost` URLs (`https://user:pass@…`) keep working, and no longer appear in results links: 0.x printed them in the CI log and posted them as the GitHub status's details link. See [bundlewatchServiceHost](https://bundlewatch.io/reference/configuration#bundlewatchservicehost). ### Base branch on GitHub Actions - Pull request builds already compared with the pull request's target branch. - Other builds, such as pushes, now compare with the repository's default branch instead of `master` (issues [#423](https://github.com/bundlewatch/bundlewatch/issues/423) and [service #280](https://github.com/bundlewatch/service/issues/280)). - Setting `CI_BRANCH_BASE` or `ci.repoBranchBase` still overrides this. If you set `CI_BRANCH_BASE` only to avoid `master`, remove it: it also replaces the target branch of pull requests. See [Base and tracked branches](https://bundlewatch.io/guide/diffing-against-branches-other-than-master). ### Pull request statuses on GitHub Actions - Statuses are now reported on the pull request's head commit. - Previously they went to `GITHUB_SHA`, a temporary merge commit, so they didn't appear on the pull request unless you set `CI_COMMIT_SHA` yourself. Setting it still works. ### Tracked branches - The default `trackBranches` is now `['master', 'main', 'develop']`, plus the repository's default branch on GitHub Actions. - `trackBranches` and `files` arrays in your configuration now replace the defaults. Previously they were merged index by index, so `trackBranches: ['main']` actually tracked `['main', 'develop']`. ### Pull request builds no longer save baselines Pull request builds compare with the baseline but no longer save one. Previously, a pull request whose head branch had a tracked name, such as a fork's `master`, overwrote the real `master` baseline with its own sizes. See [Pull request builds](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#pull-request-builds) for which builds count. ### Numeric `maxSize` A numeric `maxSize` such as `102400` is now enforced. Previously it was silently ignored and the check always passed. If your bundle is over a numeric limit, BundleWatch now reports it. `0`, whether a number or a string such as `'0'` or `'0kB'`, still means no limit. ### Other fixes - On GitHub Actions push builds, branch names containing `/` are read correctly: 0.x read a push to `feat/foo` as `feat` (issue [#422](https://github.com/bundlewatch/bundlewatch/issues/422)). - A run where some files are missing or can't be read no longer sends a baseline. The service rejected those anyway; the CLI now logs why. - With `compression: 'none'`, empty files are measured as 0 B instead of failing with `Could not read file`. gzip and brotli already measured them, as 20 B and 1 B. - Removed files named after an `Object.prototype` property, such as `constructor`, now show as removed instead of passing with no size. New files with those names get a zero-byte baseline. A file whose normalized path is exactly `__proto__` is now measured and checked instead of dropped. - Running BundleWatch in a directory without a package.json no longer crashes (issue [#434](https://github.com/bundlewatch/bundlewatch/issues/434)). - `-V` and `--version` print the version. Other flags work as before, including a bare `--max-size` for no limit; see [Command line](https://bundlewatch.io/reference/cli). - With `CI` set but no `CI_REPO_OWNER` and `CI_REPO_NAME`, BundleWatch falls back to `GIT_URL` instead of using the repository `undefined/undefined`. - BundleWatch now discovers `bundlewatch.config.*` files before falling back to `package.json`. It searches from the current directory up to and including the nearest `package.json` directory, checking `.mjs`, `.js`, `.cjs`, `.mts`, `.ts`, `.cts` and `.json` in that order in each directory. The first file wins; without a package, the search continues to the filesystem root. A broken config fails without falling back. `--config` overrides discovery and accepts absolute paths. It loads `.mjs` files, and configs with only named exports, with `import()`, and it loads `.ts`, `.mts` and `.cts` files on Node.js 22.18 and later. See [Using a config file](https://bundlewatch.io/guide/using-a-config-file). ### Proxy variables BundleWatch no longer reads `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` or similar variables: 0.x used them only as a side effect of its HTTP library, and never documented them. Behind a proxy that blocks direct connections, see [Troubleshooting](https://bundlewatch.io/guide/troubleshooting#behind-a-proxy). ### Brotli Brotli compression works without installing `brotli-size`, which you can remove. Node's built-in `zlib` produces identical sizes. ### GitHub Enterprise Commit statuses go to the GitHub API at `GITHUB_API_URL` when it is set; GitHub Actions on GitHub Enterprise Server sets it automatically. See [GitHub Enterprise](https://bundlewatch.io/guide/using-a-custom-server#github-enterprise) for the rest of the setup. ### Upgrading from 0.3.x with `normalizeFilenames` 0.3.x saved every file that the pattern didn't match under an empty name. On the first 1.0 run those files show as new, and an empty-named entry shows as removed, until a tracked branch saves a new baseline. ## Service changes (service.bundlewatch.io) These apply to every CLI version, 0.x included. > **Warning** > > Before service.bundlewatch.io enforces the stricter check below, replace tokens of users without push access: create a new one at [`/setup-github`](https://service.bundlewatch.io/setup-github) as a user with push access, or [use `GITHUB_TOKEN`](https://bundlewatch.io/guide/migrating-to-v1#using-the-github-actions-token) on GitHub Actions. - `POST /results` creates a short link and needs write access (below); `GET /r/{id}` renders it. - Saving baselines and creating short links need stricter authorization, rolled out in report-only mode: - To save, a token on a public repository must belong to a user with push access, or be an installation token for that repository, such as the `GITHUB_TOKEN` of its own workflows. - Previously, any GitHub token could overwrite a public repository's baseline. - At first the service only logs requests the new rule would refuse. - In report mode, the stricter check never changes a decision, even when it fails or times out. - Reading a baseline works as before. - Error codes changed: - Malformed results links return `400` instead of `500`. - Failures talking to GitHub return `503` instead of `401` or `500`. - Baselines too large to store return `413` instead of `500`. - The service now accepts baselines and links where `maxSize` is a size string such as `"100KB"`. 0.x rejected these even though its CLIs sent them for a numeric `maxSize` in the config. - `/setup-github` checks an OAuth `state`. An authorization started in another browser, or more than 10 minutes earlier, has to be restarted. - Deployment uses AWS SAM and GitHub Actions OIDC, replacing the Serverless Framework v3 and CircleCI. See [`packages/service/README.md`](https://github.com/bundlewatch/bundlewatch/blob/main/packages/service/README.md). ## Using the GitHub Actions token If you run BundleWatch on GitHub Actions with a token from `service.bundlewatch.io/setup-github`, you can switch to the token GitHub creates for each workflow run: grant the workflow `statuses: write`, keep `contents: read`, and pass `secrets.GITHUB_TOKEN` as `BUNDLEWATCH_GITHUB_TOKEN`. The service accepts this token for saving baselines and creating short links. See [GitHub Actions](https://bundlewatch.io/guide/using-bundlewatch#github-actions) for a complete workflow. --- Source: https://bundlewatch.io/reference/configuration # Configuration reference Set these options in a [config file](https://bundlewatch.io/guide/using-a-config-file), such as `bundlewatch.config.mjs`, and run `npx bundlewatch`. The command line requires `files`; every other option has a default. Configuration under the `bundlewatch` key in [package.json](https://bundlewatch.io/guide/using-a-config-file#package-json) is also supported. Your configuration is merged with the defaults below. Arrays, such as `files` and `ci.trackBranches`, replace the default value instead of being merged with it. | Option | Default | Description | | ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `files` | None | The files to check and their limits, see [files](https://bundlewatch.io/reference/configuration#files) | | `prefix` | Unset | Application namespace for GitHub statuses, baselines and results, see [prefix](https://bundlewatch.io/reference/configuration#prefix) | | `defaultCompression` | `'gzip'` | Compression for files that don't set their own, see [defaultCompression](https://bundlewatch.io/reference/configuration#defaultcompression) | | `normalizeFilenames` | `null` | Removes hashes from file names, see [normalizeFilenames](https://bundlewatch.io/reference/configuration#normalizefilenames) | | `bundlewatchServiceHost` | `'https://service.bundlewatch.io'` | The service that stores baselines; `null` disables it, see [bundlewatchServiceHost](https://bundlewatch.io/reference/configuration#bundlewatchservicehost) | | `ci` | Detected from your CI environment | Token, repository, commit and branches, see [ci](https://bundlewatch.io/reference/configuration#ci) | ## `prefix` **Requires BundleWatch 1.1.0 or later**, for both the CLI and Node API. Use a dependency such as `"bundlewatch": "^1.1.0"` and ensure CI runs that installed version. Older versions silently ignore `prefix`: applications share the unprefixed baseline and the `bundlewatch` GitHub status, so one application's results can overwrite another's. Do not downgrade a prefix-enabled application below 1.1.0. Use a top-level `prefix` to run BundleWatch independently for multiple applications in one GitHub repository. It works in config files, `package.json`, and the Node API (`await bundlewatch({ prefix: 'storefront', files: [...] })`). A prefix must be **1–64 lowercase ASCII letters, digits, hyphens or underscores**, starting with a letter or digit. For example, `storefront`, `admin`, and `web_v2` are valid. Empty strings, `null`, uppercase letters, spaces, slashes, and non-string values are rejected by both the client and service. Values are never trimmed or converted. Omit the option to use the existing `bundlewatch` status and baselines. For a monorepo with two frontends, give each application its own config: ```js // apps/storefront/bundlewatch.config.mjs export default { prefix: 'storefront', files: [{ path: 'dist/assets/*.js', maxSize: '200 kB' }], } ``` ```js // apps/admin/bundlewatch.config.mjs export default { prefix: 'admin', files: [{ path: 'dist/assets/*.js', maxSize: '200 kB' }], } ``` Build and run BundleWatch from each application's directory. Paths are relative to the working directory, so both applications can measure `dist/assets/app.js` without sharing a baseline: ```sh (cd apps/storefront && npm run build && npx bundlewatch) (cd apps/admin && npm run build && npx bundlewatch) ``` The overall GitHub contexts are `storefront/bundlewatch` and `admin/bundlewatch`. Per-file failures use the same prefix, such as `storefront/bundlewatch dist/assets/app.js`; path shortening, collision suffixes and status limits stay the same. Set branch protection to require the two prefixed contexts if both applications must pass. Results pages display the application name. Baselines are identified by **repository + prefix + branch**. Pull requests compare against their own application's base-branch baseline and never save baselines, even when the head branch is tracked. Neither application reads or replaces an unprefixed baseline. Changing or removing a prefix selects a different namespace; it does not move data. Build each application's tracked base branch once to establish its baseline. ### Service rollout Deploy the updated service to **every instance first**, then upgrade clients and enable `prefix`. Existing clients and unprefixed configurations keep their current requests, storage keys, and links; no migration is needed. Before a prefixed client reads or saves a baseline, it sends `POST /capabilities` and requires `{ "baselinePrefix": 1 }`. Failure, an unreachable endpoint, or a different response stops the run with instructions to upgrade or select a compatible `bundlewatchServiceHost`. The client never falls back to unprefixed baselines. With the service disabled (`bundlewatchServiceHost: null`), prefixes still work for GitHub statuses without a capability check. Prefixed clients use `/store/prefixed/lookup` and `/store/prefixed`, and check that responses identify the requested prefix. These dedicated routes prevent an old instance from interpreting a prefixed request as a legacy baseline write during a partial rollout or rollback. Keep the capable service deployed while prefixed clients run; rolling it back can fail their checks. Do not remove a prefix just to bypass the compatibility error, since that selects the legacy namespace. ## `files` Each entry in `files` checks the files that match its `path`: | Field | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | Required. A glob, relative to the directory you run BundleWatch in, not to the config file. `*`, `**` and `{a,b}` work. A path that matches no files fails the run, and so does one that matches a directory: `./dist/*` fails as soon as `dist` contains a folder, so write `./dist/*.js` or `./dist/**/*.js`. | | `maxSize` | Optional. The limit for each matching file, not for their total: a file larger than its `maxSize` fails. Write a string with a unit, such as `'100kB'`, or a number of bytes, such as `102400`. | | `compression` | Optional: `'gzip'`, `'brotli'` or `'none'`. Defaults to [defaultCompression](https://bundlewatch.io/reference/configuration#defaultcompression). Any other non-empty value isn't rejected: the file is then measured uncompressed. | `maxSize` values: - Units are `B`, `KB`, `MB`, `GB`, `TB` and `PB`. They are case-insensitive and use multiples of 1024, so `'100kB'` is 102400 bytes. - A string with any other unit is read as a number of bytes: `'100k'` and `'100KiB'` both mean 100 bytes. - When `maxSize` is missing, `0` (a number, or a string such as `'0'` or `'0kB'`), or doesn't start with a number, the file has no size limit. Growth compared with the baseline never fails the check: only `maxSize` does. If two entries match the same path, such as `./dist/*.js` and `./dist/app.js`, the `maxSize` and `compression` of the last one apply: put catch-all globs first. ## `defaultCompression` The compression used to measure files that don't set their own `compression`: `gzip` (default), `brotli` or `none`. gzip runs at level 9, and brotli at quality 11. Any other value stops the run with `[FATAL] config.compression must be a valid type`. Brotli uses Node's built-in `zlib`; no extra package is needed. ## `normalizeFilenames` Removes the part of each file name that changes between builds, such as a content hash, so that each file can be compared with its baseline. It changes the names BundleWatch reports and saves, not your files. Set it to a `RegExp`, or to a string that BundleWatch turns into one. Write a pattern that matches the whole path, from `^` to `$`, and capture the part to remove: BundleWatch removes the captured text. A path that the pattern doesn't match keeps its name. | Path | Pattern | Becomes | | --------------------------------- | -------------------------- | ------------------------ | | `./dist/main.3f2a1b9c.js` | `/^.*(\.[a-f0-9]+)\.\w+$/` | `./dist/main.js` | | `./dist/assets/index-BdF3x9Qa.js` | `/^.*?(-[\w-]{8})\.\w+$/` | `./dist/assets/index.js` | In JSON, write the pattern as a string without the slashes, and double each backslash: `"normalizeFilenames": "^.*(\\.[a-f0-9]+)\\.\\w+$"`. > **Warning** > > Check the result against your real file names. The second pattern also turns `./dist/app-polyfill.js` into `./dist/app.js`. `/^.+?(\..+?)\.\w+$/`, an older example, turns `./.next/static/chunks/main-abc123.js` into `./.js`. > > When two files end up with the same name, only the last one is checked and saved, and nothing is logged. > **How the pattern is applied** > > BundleWatch splits the path with the pattern. That gives the text before the match, the text after it, and each captured group. BundleWatch then removes the first occurrence of each of these pieces from the path. > > With a pattern that matches the whole path, that removes the captured text. A pattern that matches only part of the path also removes everything outside the match: `/-[a-f0-9]+/` turns `./dist/main-abc123.js` into `-abc123`. ## `bundlewatchServiceHost` The service that stores baselines, creates short results links and renders results pages. Defaults to `'https://service.bundlewatch.io'`. Set it to your own service's URL to use a [custom server](https://bundlewatch.io/guide/using-a-custom-server). Give the full URL, starting with `https://`, without a trailing slash: BundleWatch appends paths such as `/store` to it. It can include Basic auth credentials, such as `https://user:password@bundlewatch.example.com`: BundleWatch sends them in an `Authorization` header, and leaves them out of results links and log lines. Set it to `null` to disable the service: BundleWatch then only checks `maxSize`, without comparing with or saving a baseline. It prints no results link, the GitHub status has no details link, and the [Node.js API](https://bundlewatch.io/reference/nodejs#result) returns `url: undefined`. ## `ci` The `ci` options let BundleWatch post [GitHub statuses](https://bundlewatch.io/guide/using-bundlewatch#github-statuses), and compare with and save baselines. Most defaults are detected from your CI environment; set an option only to override it. | Option | Default | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `githubAccessToken` | `BUNDLEWATCH_GITHUB_TOKEN`, then `GITHUB_ACCESS_TOKEN` | | `repoOwner` | Detected, e.g. `facebook` in github.com/facebook/react | | `repoName` | Detected, e.g. `react` in github.com/facebook/react | | `repoCurrentBranch` | Detected | | `repoBranchBase` | Detected: the branch a pull request is being merged into, or on GitHub Actions the repository's default branch. Falls back to `'master'`. | | `commitSha` | Detected | | `trackBranches` | `['master', 'main', 'develop']`, plus the repository's default branch when it can be detected | | `isPullRequest` | Detected. Pull request builds compare with a baseline but never save one; see [which builds count](https://bundlewatch.io/guide/using-bundlewatch#supported-ci-providers) | Builds on a branch listed in `trackBranches` save their results as the baseline for that branch, unless they are pull request builds. See [Base and tracked branches](https://bundlewatch.io/guide/diffing-against-branches-other-than-master). ## Environment variables BundleWatch reads these environment variables. Values in your configuration come first. `CI_REPO_OWNER`, `CI_REPO_NAME`, `CI_REPO_SLUG`, `CI_COMMIT_SHA`, `GIT_COMMIT`, `CI_BRANCH`, `GIT_BRANCH` and `CI_BRANCH_BASE` take precedence over what BundleWatch detects from your CI provider. `GIT_URL` is used only when nothing else gives the repository, and `CI_EVENT` and `CI_TARGET_BRANCH` are read for generic CI: when `CI` is set and no supported provider is detected. | Variable | Sets | | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BUNDLEWATCH_GITHUB_TOKEN`, then `GITHUB_ACCESS_TOKEN` | `ci.githubAccessToken` | | `CI_REPO_OWNER`, `CI_REPO_NAME` | `ci.repoOwner`, `ci.repoName` | | `CI_REPO_SLUG` (`owner/name`) | `ci.repoOwner` and `ci.repoName`, if the variables above are not set | | `CI_COMMIT_SHA`, then `GIT_COMMIT` | `ci.commitSha` | | `CI_BRANCH`, then `GIT_BRANCH` | `ci.repoCurrentBranch` | | `CI_BRANCH_BASE` | `ci.repoBranchBase` | | `CI_TARGET_BRANCH` | `ci.repoBranchBase` with generic CI, when `CI_BRANCH_BASE` isn't set | | `GITHUB_API_URL` | The GitHub API URL that commit statuses are posted to. Defaults to `https://api.github.com`; GitHub Actions sets it automatically. GitHub Enterprise also needs your own service, see [GitHub Enterprise](https://bundlewatch.io/guide/using-a-custom-server#github-enterprise). | | `CI_EVENT` | `pull_request` marks a [pull request build](https://bundlewatch.io/guide/diffing-against-branches-other-than-master#pull-request-builds) with generic CI | | `GIT_URL` | `ci.repoOwner` and `ci.repoName`, from a github.com remote such as `https://github.com/owner/repo.git` or `git@github.com:owner/repo.git`, if nothing else sets them | | `DEBUG` (any non-empty value) | Logs the details of failed requests to the BundleWatch service | | `NO_COLOR`, `NODE_DISABLE_COLORS`, `FORCE_COLOR` | Output is colored only in a terminal. `NO_COLOR` or `NODE_DISABLE_COLORS` turns colors off. `FORCE_COLOR` turns them on, for example in a CI log viewer that supports them, unless it is `0` or `false`; it wins over the other two. | BundleWatch detects the other values on GitHub Actions, GitLab CI, Travis CI, Drone, CircleCI and several other CI providers. See [Supported CI providers](https://bundlewatch.io/guide/using-bundlewatch#supported-ci-providers). --- Source: https://bundlewatch.io/reference/cli # Command line ``` Usage: bundlewatch [options] ``` Run it with `npx bundlewatch` (or your package manager's equivalent) after [installing](https://bundlewatch.io/guide/getting-started#installation) it. ## Options | Option | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--config [configFilePath]` | File to read configuration from: `.json`, `.js`, `.cjs` or `.mjs`, or `.ts`, `.mts` or `.cts` on Node.js 22.18 and later. Any other extension is read as JSON. Relative paths are resolved from the current directory. See [Using a config file](https://bundlewatch.io/guide/using-a-config-file). | | `--max-size [maxSize]` | Limit for each file given on the command line, such as `3kb`. See [`maxSize`](https://bundlewatch.io/reference/configuration#files) for the units. Without a value, there is no limit. | | `--compression [compression]` | Compression for the files given on the command line: `gzip` (default), `brotli` or `none`. | | `--normalize [regex]` | Sets [`normalizeFilenames`](https://bundlewatch.io/reference/configuration#normalizefilenames) for the files given on the command line. | | `-V, --version` | Output the version number | | `-h, --help` | Output usage information | - `--max-size`, `--compression` and `--normalize` only apply to file globs given on the command line. With a discovered config file, `--config`, or the `bundlewatch` key in package.json, they're ignored without a warning. - An option takes the next argument as its value, unless that argument starts with `-`. Put the value right after the option, or write `--max-size=3kb`: in `bundlewatch --max-size ./dist/*.js`, the first file becomes the size. - Without a value: with file globs, `--max-size` means no limit, and `--compression` or `--normalize` stops the run with an error. Without file globs, `--config` stops the run with an error. - `--` ends the options: every argument after it is a glob. - An unknown option prints `unknown option '…'` and exits with code 1. ## Examples Discover a config file (recommended): ```bash npx bundlewatch ``` If no config file is discovered, the same command reads the `bundlewatch` key in `package.json`. To use a specific file instead: ```bash npx bundlewatch --config internals/bundlewatch.config.mjs ``` Use the command line: ```bash npx bundlewatch --max-size 100kB "./src/*.js" "./lib/*.js" ``` Quote globs so that BundleWatch expands them, not your shell: `**` then works in every shell. ## Where configuration comes from BundleWatch uses the first of these it finds: 1. File globs on the command line, with `--max-size`, `--compression` and `--normalize`. 2. The file given with `--config`. 3. A discovered `bundlewatch.config.*` file. Search starts in the current directory and includes parents up to the nearest `package.json`; see [config discovery](https://bundlewatch.io/guide/using-a-config-file#config-discovery) for filenames and search boundaries. 4. The `bundlewatch` key in the nearest `package.json`. It can be a config object, or an array that is used as `files`. An unused `--config` option or `package.json` configuration triggers a warning. Discovery is skipped when globs or `--config` are supplied. If a selected config file cannot be loaded or is invalid, the run fails without falling back. If none is found, it prints `[ERROR] Configuration missing` and exits with code 1. ## Exit codes | Code | When | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `0` | Every file passes (`bundlewatch PASS`), or `--help` or `--version` was given. | | `1` | A file fails its check (`bundlewatch FAIL`), the configuration is invalid or missing, an option is unknown, or an unexpected error occurs. | A file fails when it is over its `maxSize`, when a path matches no files, or when a matched path can't be read, such as a directory. Errors from the BundleWatch service, and error responses from GitHub, are logged without changing the exit code. If statuses are enabled (a token, the repository and the commit are known) and GitHub can't be reached or doesn't answer within 5 seconds, the run fails with code 1. [Troubleshooting](https://bundlewatch.io/guide/troubleshooting) explains each message. ## Output Results and `[INFO]` and `[WARNING]` lines go to stdout; `[ERROR]` and `[FATAL]` lines go to stderr. The details printed before an `[ERROR]` or `[FATAL]` line, such as `Error: ECONNABORTED`, and `[DEBUG]` lines, go to stdout. To turn colors on or off, see [environment variables](https://bundlewatch.io/reference/configuration#environment-variables). --- Source: https://bundlewatch.io/reference/nodejs # Node.js API `bundlewatch(config)` takes the same options as the [configuration](https://bundlewatch.io/reference/configuration), and detects the `ci` values from the environment like the CLI does. It doesn't read package.json or a config file. The package requires Node.js 22.17 or later, and ships ES module and CommonJS builds with [TypeScript types](https://bundlewatch.io/reference/nodejs#typescript). ```js import bundlewatch, { STATUSES } from 'bundlewatch' const results = await bundlewatch({ files: [{ path: './dist/*.js', maxSize: '100kB' }], }) console.log(results.summary) if (results.status === STATUSES.FAIL) { process.exitCode = 1 } ``` Like the CLI, the API posts GitHub statuses and compares with and saves baselines when the `ci` details are available. It also logs the same `[INFO]`, `[WARNING]` and `[ERROR]` lines to the console, but doesn't print the results or set the exit code: that's up to you. Unlike the CLI, it accepts an empty `files`. Nothing is measured and the result is `'pass'`, but a tracked-branch build with a token still saves, which replaces the baseline with an empty one. ## CommonJS In CommonJS code, use the `default` export: ```js const bundlewatch = require('bundlewatch').default const { STATUSES } = require('bundlewatch') ``` `require()` loads the package's CommonJS build. ## Result `bundlewatch()` returns a promise for a result like this one: ```js { status: 'fail', fullResults: [ { filePath: './dist/app.js', message: '30.71KB < 100KB (gzip)', status: 'pass', size: 31448, baseBranchSize: 0, maxSize: 102400 }, { filePath: './dist/vendor.js', message: '198.6KB > 100KB (gzip)', status: 'fail', size: 203368, baseBranchSize: 0, maxSize: 102400 } ], summary: 'maxSize check failed', url: 'https://service.bundlewatch.io/r/3fK9xQ2LmP7a' } ``` | Field | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `status` | `'pass'` or `'fail'`, also available as `STATUSES.PASS` and `STATUSES.FAIL`. `'warn'` (`STATUSES.WARN`) is reserved: no check produces it. | | `summary` | A one-line summary, e.g. `'maxSize check failed'` | | `url` | The [results page](https://bundlewatch.io/guide/using-bundlewatch#results-links): a short link when the service accepts the token for writing, a long one otherwise. `undefined` when `bundlewatchServiceHost` is `null` | | `fullResults` | One entry per distinct path: each measured file, each path that matched nothing or couldn't be read, and each baseline file missing from this build. Files that end up with the same name after [`normalizeFilenames`](https://bundlewatch.io/reference/configuration#normalizefilenames) share one entry. | Each entry in `fullResults` has: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `filePath` | The file path, after `normalizeFilenames`. Error entries have the glob that matched nothing, or the path that couldn't be read. | | `status` | `'pass'`, `'fail'`, or `'removed'` (`STATUSES.REMOVED`) for a file that is in the baseline but no longer exists | | `message` | The result as text. Missing on error entries. | | `error` | Only on error entries, e.g. when a path matches no files. They have no sizes, and their `status` is `'fail'`. | | `size` | Size in bytes, after compression. `0` for removed files. | | `baseBranchSize` | Size in the baseline, in bytes. `0` when there is no baseline. | | `maxSize` | Limit in bytes. `Infinity` when not set; `0` for removed files. | ## TypeScript Both builds include type declarations. Besides `bundlewatch` and `STATUSES`, the package exports these types: | Type | Description | | ------------------- | ------------------------------------------------------- | | `BundlewatchConfig` | The configuration passed to `bundlewatch()` | | `FileConfig` | An entry in `files` | | `CiConfig` | The `ci` object | | `BundlewatchResult` | What `bundlewatch()` resolves to, see [Result](https://bundlewatch.io/reference/nodejs#result) | | `FileResult` | An entry in `fullResults` | | `Status` | `'pass'`, `'warn'`, `'fail'` or `'removed'` | ```ts import bundlewatch, { type BundlewatchConfig } from 'bundlewatch' const config: BundlewatchConfig = { files: [{ path: './dist/*.js', maxSize: '100kB' }], } const results = await bundlewatch(config) ``` ## Errors When the configuration is invalid, `bundlewatch()` rejects with a `ValidationError`, which the package also exports. As in 0.x, its `type` is `'ValidationError'`. A file over its limit doesn't reject: the result's `status` is `'fail'`. `bundlewatch()` rejects when it can't post a GitHub status, because GitHub can't be reached or doesn't answer within 5 seconds. The error's message is the code, such as `ECONNABORTED`, and if this happens after the pending status, it first tries to set `bundlewatch` to error. Errors from the BundleWatch service never reject: they're logged. A failed lookup leaves the result without a comparison, and a failed short link leaves `url` as the long link. ```js import bundlewatch, { ValidationError } from 'bundlewatch' try { await bundlewatch({ files: [{ path: './dist/*.js' }], defaultCompression: 'zip', }) } catch (error) { if (error instanceof ValidationError) { console.error(error.message) // config.compression must be a valid type } else { throw error } } ```