# Packages from Internal Registries in Playwright Check Suites - Checkly Docs

> Install private packages from registries Checkly cannot reach, such as registries behind your firewall or VPN, by embedding them in the code bundle, pruning what the runner installs, or routing installs to other registries.

Source: https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/internal-registries/

---

Back to top
- Embed private packages Supported lockfiles
- Validation
- Registry credentials
- Code bundle size

- Pruning Lockfile pruning
- Prune manifest dependencies
- Target specific workspace members
- Requirements and caveats

- Route runner installs to other registries
- Dependencies of dependencies Embed private dependency chains

- Examples Monorepo with an internal registry
- Private scope behind a VPN-only mirror

- Package name patterns
- Troubleshooting See what was embedded or pruned
- The code bundle is too large to upload
- Dependencies are reinstalled after a change

Playwright Check Suites

# Packages from Internal Registries in Playwright Check Suites

Install private packages from registries Checkly cannot reach, such as registries behind your firewall or VPN, by embedding them in the code bundle, pruning what the runner installs, or routing installs to other registries.

When you run `npx checkly test` or `npx checkly deploy`, the CLI packs your Playwright tests into a code bundle together with the files your package manager needs: `package.json` files, the lockfile, and `.npmrc`. Before each run, Checkly installs your dependencies from that bundle, fetching packages from the registries your project uses.
That install fails when your project depends on private packages from an internal registry that Checkly can’t reach, such as an Artifactory, Nexus, or Verdaccio instance behind your firewall or VPN, or an intranet-only mirror of the public npm registry. This page covers the CLI options for those setups.

If your private registry is reachable from the internet and only needs a token, such as GitHub Packages or a hosted npm organization, you don’t need to embed or route packages. Configure your `.npmrc` as described in [Private packages](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies#private-packages).

Choose the option that matches your setup:
Situation Option

Your private packages are only available from an internal registry Checkly can’t reach Embed the packages with `bundle.packages.embed`

Your `.npmrc` points at an internal mirror, but the packages are also available from registries Checkly can reach Route runner installs with `runner.registries`

Your monorepo’s lockfile includes dependencies of workspace members your checks don’t import Lockfile pruning leaves them out automatically

The install pulls in packages from your internal registry that your checks don’t use, such as unused peer dependencies Prune manifest dependencies with `bundle.packages.prune`

All of these options apply to Playwright Check Suites only, not to Browser Checks or Multistep Checks. You configure them at the top level of `checkly.config.ts`, not per check.

## ​ Embed private packages

If some of your dependencies come from a private registry that Checkly’s infrastructure can’t reach, list them in `bundle.packages.embed`. The CLI downloads those packages on the machine where you run it, verifies each tarball against the integrity hash in your lockfile, and ships them inside the code bundle. The runner then installs them without contacting your registry.
checkly.config.ts

```
import { defineConfig } from 'checkly'

export default defineConfig({
projectName: 'Acme Website',
logicalId: 'acme-website',
bundle: {
packages: {
embed: [
'@acme/*',
'!@acme/public-sdk',
'legacy-private-pkg@2.1.0',
],
},
},
checks: {
playwrightConfigPath: './playwright.config.ts',
playwrightChecks: [
// ...
],
},
})
```

This config embeds every package in the `@acme` scope that your lockfile contains, except `@acme/public-sdk`, which the runner must then fetch itself. If your `.npmrc` sends the whole `@acme` scope to your internal registry, route `@acme/public-sdk` to a registry the runner can reach with `runner.registries`. It also embeds version 2.1.0 of the unscoped `legacy-private-pkg`. Every other package installs from its usual registry.
Each entry is one of:

- A package name, such as `'@acme/private-utils'`. Embeds every version of the package found in the lockfile.

- An exact version pin, such as `'legacy-private-pkg@2.1.0'`. Checkly lists only the embedded versions of an embedded package, so a pin must cover every version of that package in your lockfile. Otherwise the install can fail; npm, for example, can’t find the missing version. `checkly test` and `checkly deploy` warn when an entry leaves versions of a package in your lockfile unembedded. Embed the package by name to include every version in your lockfile.

- A name pattern, such as `'@acme/*'`, optionally prefixed with `!` to exclude packages that earlier entries selected.

List every package the runner can’t fetch, including private packages that are only dependencies of other private packages. Embedding a package doesn’t embed its dependencies; see Dependencies of dependencies.

### ​ Supported lockfiles

The CLI resolves entries against the lockfile at the workspace root:
Lockfile Supported

`pnpm-lock.yaml` ✅

`package-lock.json` ✅

`bun.lock` (text format) ✅

`yarn.lock` (Yarn Berry, v2 and later) ✅

`yarn.lock` (Yarn Classic, v1) ❌

`bun.lockb` (binary format) ❌ Run `bun install --save-text-lockfile` to create a `bun.lock`

Yarn Berry lockfiles don’t record npm tarball hashes, so for Yarn Berry the CLI looks them up in your registry’s package metadata each time it bundles your checks. Downloaded tarballs are still cached.

### ​ Validation

The CLI validates the list before deploying:

- An entry that matches no package in the lockfile fails validation and blocks the deploy. Only list packages your project actually uses.

- An entry whose only matches can’t be embedded also fails validation. Packages installed from git, a local path, or a URL, and lockfile entries without an integrity hash, can’t be embedded; the runner must fetch them itself.

- Workspace members that a pattern matches are skipped, because they’re bundled as source code rather than embedded. An entry that matches only workspace members fails validation.

- A `!` exclusion that matches nothing is ignored.

- A list that ends up selecting no packages at all only prints a warning.

### ​ Registry credentials

Embedded packages need registry credentials only on the machine running the CLI, which downloads them. The runner installs them from the code bundle and never contacts your registry for them, so you don’t need to store your registry token in Checkly; a token reference in the bundled `.npmrc` whose variable isn’t set in Checkly doesn’t break the install. The CLI reads credentials from:

- The project, workspace root, and user `.npmrc` files

- `npm_config_*` environment variables

- pnpm’s global `auth.ini`, where `pnpm login` stores tokens on pnpm 11 and later

Scope tokens to the registry (`//registry.example.com/:_authToken=${NPM_TOKEN}`) rather than setting a bare `_authToken`. Because `.npmrc` is uploaded with the code bundle, reference tokens through environment variables instead of writing them in plain text, and set those variables wherever you run `checkly test` or `checkly deploy`. Only packages the runner fetches itself need their token stored in Checkly, as described in [Private packages](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies#private-packages). If you use `runner.registries`, set those tokens in its `auth` entries instead.
If you use bun or Yarn Berry and your credentials only live in `bunfig.toml` or `.yarnrc.yml`, copy them into `.npmrc` or set them as `npm_config_*` variables; the CLI doesn’t read those files. bun and Yarn Berry projects must also keep their registry URLs, including scoped registries, in `.npmrc`.
If a download fails to authenticate, the error names the credential source the CLI used.

### ​ Code bundle size

The Checkly API accepts code bundles of up to 30 MB, compressed. Embedded packages ship inside the code bundle, so embedding large packages, or many packages that add up, can push the bundle over that limit. `checkly test`, `checkly pw-test`, and `checkly deploy` then fail when they upload the bundle:
Terminal

```
The code bundle is too large to upload: the compressed bundle is 34.2 MB, but the Checkly API accepts at most 30 MB. Reduce the bundle size by removing large files from the Playwright project, narrowing any 'include' patterns, or embedding fewer private packages ('bundle.packages.embed').
```

To stay under the limit, embed only the packages the runner can’t fetch from a registry it can reach, and let lockfile pruning and `bundle.packages.prune` drop the packages your checks don’t use. Packages that pruning removes from the bundled lockfile aren’t embedded.
Enterprise plans can raise the limit to 100 MB. Contact your account executive or [contact us in the Checkly Web App](https://app.checklyhq.com/?support=true).

## ​ Pruning

Pruning reduces what the runner installs to what your code bundle needs. This keeps internal packages your checks never use out of the install, so you don’t have to embed them or make their registry reachable. The CLI never modifies the files on disk; it only rewrites the copies in the code bundle.

### ​ Lockfile pruning

In a monorepo, your code bundle often contains only some of the workspace members. Installing from the full lockfile would make the runner fetch dependencies of members that aren’t in the bundle, and fail if any of those come from a registry it can’t reach. The CLI therefore prunes the bundled lockfile to the bundle’s contents automatically. You don’t need to configure anything.
Lockfile pruning supports the same lockfiles as embedding, and requires your package manager to be installed on the machine running the CLI. pnpm and npm reuse their local caches and only contact your registry for packages they haven’t cached; Yarn Berry and bun prune offline.
If pruning is needed but can’t run or can’t verify its result, the CLI ships the original lockfile unchanged and prints a note explaining why. Embedding follows the pruned lockfile: a package that only workspace members outside the bundle depend on is not embedded, even if an entry matches it, because the runner never installs it.
Control lockfile pruning with these environment variables:
Variable Description

`CHECKLY_LOCKFILE_PRUNE` Set to `0` or `false` to turn lockfile pruning off. This also turns off `bundle.packages.prune` when a lockfile is bundled.

`CHECKLY_LOCKFILE_PRUNE_TIMEOUT` Time budget for pruning, in seconds. Defaults to `30`. Raise it if pruning times out, for example `CHECKLY_LOCKFILE_PRUNE_TIMEOUT=120`. Set to `0` to turn the timeout off.

If a package you need stops being embedded after pruning, the workspace member that depends on it is probably not part of the bundle. Import that member from your test code instead of turning pruning off.

### ​ Prune manifest dependencies

Use `bundle.packages.prune` to remove dependencies from the `package.json` files in the code bundle: the workspace root’s and every bundled workspace member’s. The CLI rewrites the manifests before it prunes the lockfile, so the removed packages drop out of the bundled lockfile too. Besides the peer dependency case below, you can use it to [speed up installs](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies#speed-up-installs).
The most common use is unused peer dependencies. With pnpm’s `auto-install-peers` setting, which is on by default, a member’s peer dependencies end up in the lockfile even when your tests never import them. If those peers come from a registry Checkly can’t reach, the install fails:
checkly.config.ts

```
import { defineConfig } from 'checkly'

export default defineConfig({
projectName: 'Acme Website',
logicalId: 'acme-website',
bundle: {
packages: {
prune: { peerDependencies: true },
},
},
})
```

This config removes every entry under `peerDependencies` from the workspace root’s and each bundled member’s `package.json`. When the CLI then prunes the lockfile, the peers that were only installed to satisfy those entries drop out, so the runner doesn’t try to fetch them.
`prune` accepts two forms:

- Pattern list
- Per dependency class

An array of package name patterns. Matching packages are removed from every dependency class of every bundled `package.json`: checkly.config.ts

```
bundle: {
packages: {
// Remove the @acme scope, except @acme/runtime
prune: ['@acme/*', '!@acme/runtime', 'storybook'],
},
},
```

This removes every `@acme` package except `@acme/runtime`, plus `storybook`, from `dependencies`, `devDependencies`, `peerDependencies`, and `optionalDependencies`.

An object keyed by dependency class: `dependencies`, `devDependencies`, `peerDependencies`, or `optionalDependencies`. Each value is `true` to remove the whole class, or a pattern list to remove only matching packages from that class: checkly.config.ts

```
bundle: {
packages: {
prune: {
peerDependencies: true,
devDependencies: ['@storybook/**', 'eslint*'],
},
},
},
```

This removes all peer dependencies, and removes the `@storybook` scope and unscoped packages starting with `eslint` from `devDependencies` only. `dependencies` and `optionalDependencies` are left as they are. `true` can’t be combined with `!` exclusions. To remove a whole class except some packages, use `['**', '!package-to-keep']`.

Unlike `embed`, prune patterns don’t accept version pins. A pattern that matches nothing is ignored. Removing a peer dependency also removes its `peerDependenciesMeta` entry. Patterns only match dependencies declared in `package.json` files; see Dependencies of dependencies.

#### ​ Target specific workspace members

To prune only some workspace members, use member-scoped entries in the array form. `member` is a pattern list matched against each manifest’s `name`; use `'.'` for the workspace root. Each entry has exactly one of `remove` or `keep`, which take a pattern list or a per-class object:
checkly.config.ts

```
bundle: {
packages: {
prune: [
// Remove packages from matching members only
{
member: ['@acme/web'],
remove: { peerDependencies: true },
},
// Declare the member's complete dependency set
{
member: ['@acme/design-system'],
keep: { dependencies: ['@acme/tokens'] },
},
],
},
},
```

The first entry removes the peer dependencies of `@acme/web` and leaves every other member’s peers in place. The second entry reduces `@acme/design-system` to a single dependency, `@acme/tokens`: its other `dependencies` and all of its `devDependencies`, `peerDependencies`, and `optionalDependencies` are removed.

- `remove` works like the global patterns, but only for the matched members. Entries apply in the order you list them.

- `keep` declares a member’s entire remaining dependency set. Anything it doesn’t select is removed, classes a per-class `keep` doesn’t mention are emptied, and no other entry can remove a kept package. `keep: []` removes every dependency.

Prefer exact member names with `keep`, since a wildcard can match a member you add later. The CLI warns when a `member` selector matches no workspace member, or when a `keep` pattern keeps nothing in a bundled member.

#### ​ Requirements and caveats

- When the bundle contains a lockfile, `prune` only takes effect if lockfile pruning succeeds in the same run, so the manifests and lockfile stay in sync. Otherwise the original `package.json` files ship unchanged and the CLI prints a warning. A bundle without a lockfile always ships the pruned manifests.

- The CLI doesn’t check your code against the pruned dependencies. Don’t remove anything your tests need at runtime, or anything the install itself needs. For example, keep `typescript` when a package’s `prepare` or `postinstall` script compiles TypeScript during the install (see [lifecycle scripts](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies#using-lifecycle-scripts)).

## ​ Route runner installs to other registries

If your `.npmrc` points at a registry URL that only works on your network, such as a VPN-only mirror, but the same packages are available to Checkly from other registries, use `runner.registries` instead of embedding. It tells the runner which registries to install from, replacing the registries named in the bundled configuration files:
checkly.config.ts

```
import { defineConfig } from 'checkly'

export default defineConfig({
projectName: 'Acme Website',
logicalId: 'acme-website',
runner: {
registries: {
upstreams: {
acme: {
url: 'https://npm.acme.example.com/',
auth: { type: 'bearer', token: '${ACME_NPM_TOKEN}' },
},
npmjs: { url: 'https://registry.npmjs.org/' },
},
packages: [
{ pattern: '@acme/*', upstreams: ['acme'] },
{ pattern: '**', upstreams: ['npmjs'] },
],
},
},
})
```

With this config, the runner installs `@acme` packages only from `https://npm.acme.example.com/`, authenticating with the `ACME_NPM_TOKEN` environment variable, and every other package from the public npm registry. If an `@acme` package is missing from the Acme registry, the install fails instead of asking the public registry for it.

- `upstreams` names the registries the runner may use. A `token` must be a single `${VARIABLE}` reference to a Checkly [environment variable](https://www.checklyhq.com/docs/platform/variables), which the runner resolves at install time. Literal tokens are rejected.

- Once `runner.registries` is set, the runner ignores the registry credentials and TLS settings, such as `strict-ssl` and `cafile`, in your bundled `.npmrc`, `pnpm-workspace.yaml`, and `.yarnrc.yml`. Give every upstream that needs authentication its own `auth` entry.

- `packages` routes package names to upstreams. The first rule whose pattern matches wins. The runner tries the rule’s upstreams in order, but never falls through to a later rule, so private package names aren’t sent to a broader registry. The last rule must be `{ pattern: '**', ... }`.

- Routing patterns don’t accept `!` exclusions or version pins.

- Embedded packages never go through routing rules: the runner installs them from the code bundle and doesn’t ask any routed registry about them.

`runner.registries` only affects the install on Checkly. Embedding and lockfile pruning still run on your machine and use your `.npmrc` credentials.

## ​ Dependencies of dependencies

Your lockfile records every package the install needs, including dependencies of your dependencies (transitive dependencies). Each option handles them differently:
Option Dependencies of dependencies

`bundle.packages.embed` Entries match packages at any depth in the lockfile, so you can embed a transitive dependency by name. Embedding a package doesn’t embed its dependencies.

Lockfile pruning Removes the dependencies of workspace members outside the bundle at every depth, except packages that something in the bundle still needs.

`bundle.packages.prune` Patterns only match dependencies declared in `package.json` files; a pattern that names a package that is only a transitive dependency has no effect. Removing a declared dependency also removes the transitive dependencies that only it needed from the bundled lockfile.

`runner.registries` Routing rules apply to every package the runner fetches from a registry, at any depth. Embedded packages never reach them.

### ​ Embed private dependency chains

The runner installs the dependencies of an embedded package like any other package, from the registries your project uses. That works for public dependencies, but if an embedded package depends on other private packages from the same unreachable registry, you must embed those too. The CLI doesn’t detect a missing one; the install on Checkly fails when it can’t fetch it.
For example, if `@acme/ui` depends on the private `@acme/tokens` and the public `react`, embed both private packages and let the runner fetch `react` from npm:
checkly.config.ts

```
bundle: {
packages: {
embed: ['@acme/ui', '@acme/tokens'],
},
},
```

If your private packages share a scope, a wildcard such as `@acme/*` embeds the whole scope at every depth, including private packages you only depend on indirectly.

## ​ Examples

### ​ Monorepo with an internal registry

Your tests live in one member of a pnpm workspace, and your `.npmrc` points the `@acme` scope at a registry that’s only reachable from your network:

```
acme/
├── .npmrc # Points @acme at the internal registry
├── package.json
├── pnpm-lock.yaml
└── packages/
├── e2e/ # Tests and checkly.config.ts
├── design-system/ # Imported by the tests
└── admin/ # Not used by the tests
```

`design-system` depends on the private `@acme/tokens` and declares the private `@acme/analytics` as a peer dependency, which the tests don’t use. `admin` depends on the private `@acme/billing-sdk`.
packages/e2e/checkly.config.ts

```
import { defineConfig } from 'checkly'

export default defineConfig({
projectName: 'Acme Website',
logicalId: 'acme-website',
bundle: {
packages: {
embed: ['@acme/*'],
prune: [
{
member: ['@acme/design-system'],
remove: { peerDependencies: ['@acme/analytics'] },
},
],
},
},
checks: {
playwrightConfigPath: './playwright.config.ts',
playwrightChecks: [
{ name: 'Acme E2E', logicalId: 'acme-e2e' },
],
},
})
```

When you run `npx checkly deploy`, the CLI:

- Bundles `e2e` and `design-system`, because the tests import `design-system`. `admin` is left out.

- Removes `@acme/analytics` from the bundled copy of `design-system`’s `package.json`.

- Prunes the bundled lockfile to the bundle’s contents. `@acme/billing-sdk` drops out because only `admin` needs it, and `@acme/analytics` drops out because nothing declares it anymore.

- Embeds the `@acme` packages that remain in the pruned lockfile, here `@acme/tokens`, downloading them through your `.npmrc`. The pattern also matches the workspace members, such as `design-system`; those are bundled as source code and skipped.

On Checkly, the runner installs `@acme/tokens` from the bundle and all public packages from npm. It never contacts your internal registry.
The `prune` entry isn’t required for the install to work: without it, `@acme/*` would also embed `@acme/analytics`. Pruning the unused peer saves downloading and shipping a package the tests never load. It becomes required when the runner can neither install the peer from the bundle nor fetch it itself, for example a peer installed from an internal Git server.

### ​ Private scope behind a VPN-only mirror

Your `.npmrc` sends every install through a mirror that’s only reachable on your VPN. The mirror proxies the public npm registry and also hosts your `@acme` packages:
.npmrc

```
registry=https://npm-mirror.vpn.acme.example.com/
```

checkly.config.ts

```
import { defineConfig } from 'checkly'

export default defineConfig({
projectName: 'Acme Website',
logicalId: 'acme-website',
bundle: {
packages: {
embed: ['@acme/*'],
},
},
runner: {
registries: {
upstreams: {
npmjs: { url: 'https://registry.npmjs.org/' },
},
packages: [
{ pattern: '**', upstreams: ['npmjs'] },
],
},
},
})
```

The two options split the work:

- `embed` downloads the `@acme` packages through the mirror on your machine, where the VPN is available, and ships them in the code bundle.

- `runner.registries` sends every other package to the public npm registry instead of the mirror. Embedded packages take priority over routing rules, so the `@acme` packages never reach the `**` rule.

Run `checkly test` and `checkly deploy` with the VPN connected, so the CLI can download the embedded packages.

## ​ Package name patterns

`bundle.packages.embed`, `bundle.packages.prune`, and `runner.registries` share the same pattern syntax:
Pattern Matches

`@acme/utils` Exactly `@acme/utils`

`@acme/*` Every package in the `@acme` scope

`acme-*` Unscoped packages starting with `acme-`

`*` Every unscoped package. A single `*` never crosses the `/` in a scoped name.

`**` Every package, scoped or not

`**/utils` `utils` and `utils` in any scope, such as `@acme/utils`

`!@acme/legacy` Excludes `@acme/legacy` from what earlier entries selected (`embed` and `prune` only)

Exclusions only remove packages selected by entries before them, so order matters: `['@acme/*', '!@acme/legacy']` selects the scope except `@acme/legacy`, while `['!@acme/legacy', '@acme/*']` selects the whole scope.

## ​ Troubleshooting

### ​ See what was embedded or pruned

Run the CLI with debug logging to see which packages each `embed` entry matched, and why lockfile pruning was skipped:
Terminal

```
DEBUG='checkly:cli:services:embedded-packages,checkly:cli:services:check-parser:*' npx checkly test
```

### ​ The code bundle is too large to upload

The compressed bundle exceeds the limit the Checkly API accepts for your account: 30 MB, unless it has been raised. Enterprise plans can raise it to 100 MB. Embedded packages count toward it; see Code bundle size.

### ​ Dependencies are reinstalled after a change

Changing the set of embedded packages, the pruned manifests or lockfile, or `runner.registries` invalidates the runner’s [dependency cache](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies#dependency-caching). The next run installs dependencies from scratch. A prune pattern or a `!` exclusion that matches nothing changes none of these, so it leaves the cache untouched.

Was this page helpful?

[Suggest edits](https://github.com/checkly/docs/edit/main/detect/synthetic-monitoring/playwright-checks/internal-registries.mdx)[Raise issue](https://github.com/checkly/docs/issues/new?title=Issue%20on%20docs&body=Path:%20/detect/synthetic-monitoring/playwright-checks/internal-registries)

[Dependencies in Playwright Check Suites Previous](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/dependencies)[Playwright Check Suite Environment Variables Next](https://www.checklyhq.com/docs/detect/synthetic-monitoring/playwright-checks/environment-variables)

[x](https://x.com/checklyhq)[github](https://github.com/checkly)[linkedin](https://linkedin.com/company/checkly)

[Powered by This documentation is built and hosted on Mintlify, a developer documentation platform](https://www.mintlify.com/?utm_campaign=poweredBy&utm_medium=referral&utm_source=checkly-422f444a)
