Docs menu

Package managers

How npm, pnpm, and Yarn work on Primcloud: selection order, install commands, private registries, and lockfile requirements.

On this page

The Node.js build supports npm, pnpm, and Yarn (classic and modern). One is picked per app based on what your package.json declares and which lockfile you ship. This page covers how selection works, what each manager runs, and how to point at private registries.

Using Bun? An app with a bun.lock isn't built by the Node.js toolchain at all. It's a Bun app, installed with bun install. See Bun package managers.

Supported package managers#

Primcloud walks this priority list to decide which manager runs your build:

  1. Vendored Yarn. A yarnPath entry in .yarnrc.yml pointing at a Yarn binary committed to the repo. Highest priority: used verbatim, no download.
  2. packageManager field in package.json (e.g. "packageManager": "[email protected]"). The version must be exact: this field is Corepack-compatible and Corepack rejects ranges.
  3. engines.pnpm: a semver range in package.json.
  4. engines.yarn: a semver range in package.json.
  5. engines.npm: a semver range in package.json.
  6. yarn.lock in the repo: falls back to Yarn with a default version range.
  7. Bundled npm: the npm that ships with the resolved Node.js version. This is what you get when nothing above matches.

Pick one and be explicit. The clearest signal is packageManager:

package.json
{
  "packageManager": "[email protected]"
}

Or, for a version range instead of an exact pin:

package.json
{
  "engines": {
    "node": "24.x",
    "pnpm": "10.x"
  }
}

Lockfile requirement. The build installs dependencies only when the correct lockfile for the chosen manager is present. Mixing managers (declaring pnpm but shipping package-lock.json) is a common source of surprises. Commit the lockfile that matches the manager you selected, and delete the others.

Install behavior#

The exact install command depends on the manager. All three install strictly from the lockfile, with no drift, no version resolution at deploy time.

npm. Runs npm ci. Requires package-lock.json. Deletes any existing node_modules before installing.

pnpm. Runs pnpm install --frozen-lockfile. Requires pnpm-lock.yaml. Uses pnpm's content-addressable store, which Primcloud persists between builds.

Yarn (classic, v1). Runs yarn install --frozen-lockfile. Requires yarn.lock.

Yarn (modern, v2+ / Berry). Runs yarn install --immutable. Requires yarn.lock. If you ship a vendored Yarn binary via yarnPath, the build uses it directly.

If the lockfile doesn't match the current package.json, install fails, as it would locally. Run the manager locally to regenerate the lockfile, commit it, and redeploy.

After install: the build runs your build scripts and then prunes node_modules. With npm, packages your lockfile doesn't list are removed and dev dependencies stay installed. With pnpm and Yarn, dev dependencies are removed.

Private packages and registries#

Pointing the build at a private registry uses standard package-manager configuration files. Primcloud doesn't invent its own mechanism.

npm and pnpm. Both read .npmrc at the repo root. Register the scope or registry, and reference a token via environment variable substitution so the token itself never enters the repo:

.npmrc
@my-org:registry=https://npm.pkg.example.com
//npm.pkg.example.com/:_authToken=${MY_ORG_NPM_TOKEN}

Then set MY_ORG_NPM_TOKEN in the Secrets panel on the dashboard. The value is substituted at install time; the token never appears in the build log.

For the standard GitHub Packages registry:

.npmrc
@my-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}

Yarn (classic). Uses the same .npmrc conventions. The ${VAR} substitution works the same way.

Yarn (modern, Berry). Uses .yarnrc.yml with npmScopes and npmAuthToken:

.yarnrc.yml
npmScopes:
  my-org:
    npmRegistryServer: "https://npm.pkg.example.com"
    npmAuthToken: "${MY_ORG_NPM_TOKEN}"

Warning

Never commit tokens. Set them in the Secrets panel. Like every secret, they're stored encrypted and never surfaced in build logs.

Lockfiles#

Lockfiles are required for dependency install. Without one, the build still installs the Node.js runtime and the package manager, but skips node_modules install and the build-script phase entirely. Ship a lockfile.

Which one to ship depends on the manager you chose:

Manager Lockfile Command that regenerates it
npm package-lock.json npm install
pnpm pnpm-lock.yaml pnpm install
Yarn yarn.lock yarn install

Commit the lockfile. Rebuild it locally whenever package.json changes. Delete lockfiles from other managers. Shipping both package-lock.json and pnpm-lock.yaml is a source of confusion and can lead to the wrong manager being picked (npm wins the lockfile fallback if no engines or packageManager field steers the decision).

Lockfile drift. If package.json and the lockfile disagree, install fails. Reproduce locally with npm ci / pnpm install --frozen-lockfile / yarn install --immutable. Those are the same commands the build runs, so they surface the same errors.