Files
Zoltan KochanandClaude Opus 5.5 6f95957063 docs: lint prose with Vale (#17238)
* docs: lint prose with Vale

Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.

Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
  words, "please", "note that", hedges, puffery, optional plurals,
  exclamation marks, ellipses, curly quotes, generic link text, product
  spellings, sentence-case headings, repeated words, double spaces,
  implementation history in comments; in changesets, bold or italic
  emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
  "so that" tails in changesets.
- suggestion: passive voice.

.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).

The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.

Spelling stays with cspell, which checks identifiers that Vale does not
see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: keep prose lint from flattening meaning

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:48:09 +02:00

5.8 KiB

id, title
id title
lockfile Reading `pnpm-lock.yaml`

pnpm-lock.yaml is a YAML file, but it is not always a single YAML document. Depending on what the project uses, pnpm writes either one document or two:

Document Contents
The env lockfile (first, when present) Config dependencies and the pnpm version resolved for the project, under configDependencies and packageManagerDependencies, with the packages and snapshots entries they need
The project lockfile (last, always present) The project's own dependency graph: importers with their dependencies, and the matching packages and snapshots

Both documents declare the same lockfileVersion. That field describes the schema of the entries inside a document, not how many documents the file contains, so it does not change when the env document appears.

This page is for tools that read pnpm-lock.yaml: vulnerability scanners, SBOM generators, dependency graph builders, and dependency update bots. If you only use pnpm, you don't need to care about any of this.

A two-document lockfile looks like this:

---
lockfileVersion: '9.0'

importers:

  .:
    configDependencies: {}
    packageManagerDependencies:
      pnpm:
        specifier: 11.25.0
        version: 11.25.0

packages:
  # pnpm and its platform binaries
snapshots:
  # ...
---
lockfileVersion: '9.0'

settings:
  autoInstallPeers: true

importers:

  .:
    dependencies:
      react:
        specifier: ^19.0.0
        version: 19.0.0

packages:
  # every package in the project's dependency graph
snapshots:
  # ...

Which document you need

That depends on what your tool is for:

  • The project's dependency graph: dependency update bots, graph builders, anything that asks "what does this project depend on": the last document.
  • Everything the lockfile installs: vulnerability scanners and SBOM generators: every document. Config dependencies are real npm packages, installed into node_modules/.pnpm-config, and they appear only in the env document.

Either way, load the file with a multi-document API. A single-document one (load() in js-yaml, yaml.safe_load in Python, yaml.Unmarshal into one value in Go) either raises an error or silently gives you the first document, which, in a two-document lockfile, is the env document.

The project's dependency graph

Take the last document:

import { readFile } from 'node:fs/promises'
import { loadAll } from 'js-yaml'

const lockfile = loadAll(await readFile('pnpm-lock.yaml', 'utf8')).at(-1)

This is correct for both shapes, and for every pnpm-lock.yaml pnpm has ever written.

If you would rather detect the layout than always take the last document, a file whose first line is --- has an env document. pnpm writes that leading marker only when it writes two documents.

Everything the lockfile installs

Read every document and union what you find in each:

const documents = loadAll(await readFile('pnpm-lock.yaml', 'utf8'))
const installed = documents.flatMap((doc) => Object.keys(doc.packages ?? {}))

Take each document as an inventory of its own rather than merging the documents into a single object first. Both use the . importer key, and each carries its own packages and snapshots maps, so merging them overwrites one side's importer and loses whichever graph it held.

Why the env document comes first

pnpm has to know which pnpm version to switch to, and which plugins to load, before it can do anything else, including before it has any reason to parse a dependency graph that may be several megabytes. Keeping that information in a small leading document means every command reads a few kilobytes rather than the whole file.

When a lockfile has two documents

The env document is written when either of these applies:

  • The project has config dependencies. Their integrity checksums are project content and are always recorded.
  • pnpm records the pnpm version it resolved for the project, under packageManagerDependencies, when the project declares devEngines.packageManager. Setting pmOnFail to ignore turns it off. pnpm then doesn't enforce the pinned version, so it has nothing to record.

Two-document lockfiles have existed since config dependencies were introduced.

Since v11.23.0, a frozen install no longer rewrites this block. When the pinned pnpm version is missing from the lockfile or no longer matches it, pnpm install --frozen-lockfile fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE, so a manifest whose pin was bumped without regenerating the lockfile no longer passes CI. Run pnpm install without --frozen-lockfile and commit the result.

Scanning for vulnerabilities

:::caution

A tool that reads only the first document of a two-document lockfile does not fail loudly. The env document is a structurally valid lockfile. It just describes an importer with no dependencies. Such a tool reports that the project has no dependencies, and therefore no vulnerabilities, and a CI gate built on it passes.

:::

If you scan pnpm projects:

  • Read every document, as described above. A tool that reads only the project document misses the config dependencies; a tool that reads only the env document misses everything else.
  • Verify your scanner against a two-document lockfile before you trust its output. "It reported something" is not a sufficient check here: the env document does contain packages, so a broken reader emits a plausible-looking result listing pnpm's own binaries.
  • pnpm audit reads the lockfile correctly in both shapes and needs no configuration.