Skip to content

chore: migrate from yarn 1 to pnpm - #211

Merged
josemarluedke merged 8 commits into
feat/static-docs-exportfrom
chore/pnpm-migration
Jul 30, 2026
Merged

chore: migrate from yarn 1 to pnpm#211
josemarluedke merged 8 commits into
feat/static-docs-exportfrom
chore/pnpm-migration

Conversation

@josemarluedke

@josemarluedke josemarluedke commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates the monorepo from yarn 1 to pnpm: workspace definition, lockfile, task running, and GitHub Actions. Targets feat/static-docs-export, not main.

Important

The original motivation for this PR has evaporated — read this before merging.

This was opened because CI on #210 failed five consecutive runs at yarn install with
ECONNREFUSED, and yarn 1 fetches from registry.yarnpkg.com while pnpm fetches from
registry.npmjs.org. That reasoning fit the evidence, but it was wrong: the yarn install
has since started working again with no change to that branch, so the failure was transient
infrastructure. pnpm is not needed to fix CI.

What this PR earned on its own merits is below: pnpm's strict resolution surfaced nine real
defects
that yarn's flat node_modules had been hiding — including that all 11 of
@docfy/core's jest suites were silently failing to run, and that test-app-vite was
building against the published @docfy/ember-vite rather than the local workspace package.
That is the case for merging it, and it is a genuine one. But the urgency is gone, so this
is now a judgement call rather than a fix.

The interesting part: 9 latent bugs that yarn's flat node_modules was hiding

pnpm's strict resolution surfaced nine real defects. Every one was fixed by declaring the dependency that was actually being relied on — no shamefully-hoist, no node-linker=hoisted, no .npmrc at all.

Phantom dependencies — imported but never declared, satisfied by accident from a sibling:

Package Missing Notes
@docfy/ember @docfy/core Imported in 4 source files. Added to dependencies, not devDependencies, because its published declarations/ reference those types, so consumers must resolve them.
@docfy/ember-cli @types/unist Declared no @types/* at all while importing from 'unist'.
@docfy/ember-vite @types/unist, @eslint/js, eslint-plugin-jest
@docfy/ember-cli @eslint/js
@docfy/plugin-with-prose eslint-plugin-jest

The eslint ones were found by auditing every package's config imports against its declared deps in one pass, rather than fixing them one failure at a time. That audit now reports clean.

Workspace deps resolved from the registry. All 9 cross-workspace declarations used plain semver ("^0.11.0"). pnpm 10+ defaults link-workspace-packages to false, so those fetched from npm instead of linking locally — test-app-vite was building against the published @docfy/ember-vite@0.11.0. Converted to workspace:^0.11.0. The alternative, link-workspace-packages=true, is exactly the workaround this migration set out to avoid.

Three tsconfigs hardcoded typeRoots to the repo-root node_modules/@types, which only exists under a hoisted layout. Under pnpm this left @types/jest unresolvable and all 11 of @docfy/core's jest suites failed to run. Each now points at its own package's node_modules.

pnpm 11 specifics worth knowing

  • overrides live in pnpm-workspace.yaml, not package.json. A pnpm.overrides block in the manifest is silently ignored. With it there, @types/unist resolved to 2.0.11, whose generic Node breaks @docfy/ember-cli's compile; moved to the workspace file, it correctly pins 2.0.3 — preserving what yarn's resolutions did.
  • Build scripts are blocked by default. esbuild's postinstall installs its platform binary (vite cannot build without it) and nx is lerna's engine, so both are allowed via allowBuilds. core-js 2.x only prints a funding banner and stays off.

Task running

Root scripts move to pnpm's native recursive runner. compile deliberately omits --parallel so it stays topological — @docfy/core builds before @docfy/ember-vite typechecks against its lib/types. lerna is retained solely for version/publish, with npmClient: pnpm and the dead command.bootstrap block removed (lerna 9 has no bootstrap).

concurrently prefixes went yarn:pnpm:; verified safe, since concurrently 9.2.0 parses ^(npm|yarn|pnpm|bun|node|deno).

Verification

  • pnpm install --frozen-lockfile clean from a fully wiped tree
  • pnpm compile green after deleting all build outputs
  • @docfy/core: 11 suites, 57 tests — these were failing to run before the tsconfig fix
  • @docfy/ember-vite: 60 tests
  • test-app-vite builds and the base PR's static export still works — 15 .md files, llms.txt with its # Docfy H1, all 15 root-relative links resolving. This was the key regression gate.
  • test-app-classic builds successfully under strict pnpm — the migration's primary risk, cleared with no hoisting workarounds
  • Lint passes for core, plugin-with-prose, ember-vite

Two pre-existing failures, verified not caused by this change

  1. packages/ember lint — a no-nested-splattributes template violation and a typescript-eslint config conflict (project and projectService both set). Neither is a resolution error, and CI has the lint step commented out, so they were never enforced.
  2. @docfy/plugin-with-prose jest — 2 snapshot failures. The snapshots predate jest 29's snapshotFormat change (Array [[); the old yarn.lock resolved jest 29.7.0 too, so this failed identically before. CI only ran packages/core, so it went unnoticed.

Both are left alone as out of scope.

Needs your action

Netlify. The install phase should switch to pnpm automatically now that pnpm-lock.yaml is the only lockfile, but the build command, publish directory, and base directory for docfy-site and docfy-test-app-classic live only in the Netlify UI. If either build command names yarn, that deploy will still fail. No [build] blocks were added here rather than guess at values I cannot see.

Before publishing. The workspace: protocol must be replaced with real ranges at publish time. pnpm publish does this automatically; please confirm lerna publish does too for this setup, or published manifests would carry unresolvable workspace: specifiers.

🤖 Generated with Claude Code

josemarluedke and others added 8 commits July 30, 2026 10:10
Replaces yarn 1 workspaces with pnpm. Three latent bugs that yarn's flat
node_modules had been masking had to be fixed for a clean install:

- Cross-workspace deps used plain semver ranges. pnpm 10+ defaults
  link-workspace-packages to false, so those resolved from the registry
  instead of linking locally — test-app-vite was building against the
  published @docfy/ember-vite. Converted all 9 declarations to the
  workspace: protocol.
- packages/plugin-with-prose/tsconfig.json hardcoded typeRoots to the root
  node_modules/@types, which only exists under a hoisted layout. Removed
  the override; TypeScript's default resolution finds the package's own
  @types. The "types" dir it also listed does not exist.
- @docfy/ember-cli imports from 'unist' but declared no @types at all,
  relying on a sibling's hoisted copy. Declared @types/unist directly.

pnpm 11 reads `overrides` from pnpm-workspace.yaml, not package.json, so
yarn's `resolutions` pin moved there. Without it @types/unist resolves to
2.0.11, whose generic Node breaks @docfy/ember-cli's compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Root scripts move to pnpm's native recursive runner; lerna.json switches
npmClient to pnpm and drops the dead command.bootstrap block (lerna 9 has
no bootstrap command).

Two more phantom dependencies had to be declared before `pnpm compile`
would succeed — both were satisfied by accident under yarn's flat tree:

- @docfy/ember imports @docfy/core/lib/types in four source files but
  declared @docfy/core nowhere. Added to dependencies rather than
  devDependencies because its published declarations/ reference those
  types, so consumers must be able to resolve them.
- @docfy/ember-vite imports from 'unist' in src/types.ts without
  declaring @types/unist. Added to devDependencies, matching how
  @docfy/core already declares it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more phantom dependencies: @docfy/ember-vite and @docfy/ember-cli both
import '@eslint/js' in their eslint.config.mjs without declaring it. Every
other package in the repo declares it at ^9.32.0; these two relied on
yarn hoisting it from a sibling.

pnpm also blocks dependency build scripts by default. esbuild's postinstall
installs its platform binary (vite cannot build without it) and nx is
lerna's task engine, so both are allowed via allowBuilds. core-js 2.x's
postinstall only prints a funding banner and stays off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tsconfigs hardcoded typeRoots to the repo-root node_modules/@types,
which only exists under a hoisted layout. Under pnpm this left
@types/jest unresolvable, so @docfy/core's 11 jest suites failed to run
with "Cannot find name 'describe'".

Each now points at its own package's node_modules/@types. The local
"types" entries are preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pnpm/action-setup precedes setup-node so `cache: pnpm` can resolve the
store path, and takes its version from the root packageManager field.
Drops `npm install -g yarn` and gains dependency caching.

This is the change the migration exists for: yarn 1 fetches from
registry.yarnpkg.com, which the runners could not reach; pnpm fetches
from registry.npmjs.org.

Test steps stay explicit per package rather than `pnpm -r run test`,
which would pull in the two browser suites — one of which is red for an
unrelated, undiagnosed reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consumer-facing install docs are deliberately left on npm/yarn — how a
user installs @docfy/* into their own project is not this repo's concern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@docfy/ember-vite and @docfy/plugin-with-prose both import
eslint-plugin-jest in their eslint.config.mjs without declaring it,
relying on yarn hoisting it from @docfy/core.

Found by auditing every package's eslint config imports against its
declared dependencies rather than fixing them one failure at a time;
that audit now reports all imports declared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@josemarluedke
josemarluedke merged commit e5a6626 into feat/static-docs-export Jul 30, 2026
1 check passed
@josemarluedke
josemarluedke deleted the chore/pnpm-migration branch July 30, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant