vuer_css’s .yarnrc sets --install.ignore-optional true. Jest 30’s resolver needs an optional native binding (@unrs/resolver-binding-<platform>), so ignore-optional skips it and every module lookup returns null — Jest then reports whatever module its config names first, which made this look like a ts-jest problem for a full debugging session. Affects macOS/arm64 local dev only; CI is green — the fix is a local node_modules patch, not a repo change.
Supersedes the earlier diagnosis (2026-07-30)
This note was originally titled “ts-jest@29.4.11 is a broken upstream publish (breaks yarn test:unit)“. That diagnosis was wrong. Swapping ts-jest versions changed nothing. Corrected 2026-07-31.
Second correction (2026-07-31, later the same day) — the ts-jest claim is fully RETRACTED
An intermediate revision of this note kept a section claiming “ts-jest@29.4.11 really is a broken publish, worth pinning to 29.4.10”. That is also wrong and is now retracted. The corrupt ts-jest directory I inspected came from a poisoned local Yarn cache entry, not from the registry — see The real secondary lesson — a Yarn v6 cache entry can be silently corrupt. After yarn cache clean ts-jest + a fresh install, node_modules/ts-jest/dist/index.js is present and ts-jest@29.4.11 works fine; the full unit suite passes against stock 29.4.11. Do NOT pin ts-jest to 29.4.10 — there is nothing to pin.
Scope narrowed (2026-07-31) — this is a macOS-local dev problem, not a CI problem
The root-cause mechanism below is confirmed exactly as written, but vuer_css CI is green on the same .yarnrc. Only macOS/arm64 local dev is affected. The .yarnrc change previously recommended here has been downgraded to a local workaround — see Fix — local dev environment only.
Symptom
Validation Error: Module ts-jest in the transform option was not found.
<rootDir>/node_modules/ts-jest
…and, after “fixing” ts-jest, a second face of the same bug:
Module jest-circus/build/runner.js not found
These are ONE bug, not two
The earlier note recorded the jest-circus error as “a second, unrelated devel test-infra break”. It is not. Both are the identical null-resolution failure surfacing at a different point in Jest’s startup, once the first offending config entry stops being reached.
Root cause — ignore-optional starves Jest 30’s native resolver
Dependency chain:
jest 30
└── jest-resolve@30.4.1
└── unrs-resolver@1.12.2
└── @unrs/resolver-binding-<platform> ← OPTIONAL dependency
e.g. @unrs/resolver-binding-darwin-arm64 (Apple Silicon)
@unrs/resolver-binding-linux-x64-gnu (CI / containers)
unrs-resolver ships its platform-specific native binding as an optionalDependency (the standard napi-rs distribution pattern — one optional package per platform, only the matching one installs). vuer_css’s .yarnrc contains:
--install.ignore-optional true
so Yarn never installs the binding. require('unrs-resolver') then throws “Cannot find native binding”, and jest-resolve’s Resolver.findNodeModule() degrades to returning null for every module — verified: ts-jest, jest-circus, lodash, and even jest-resolve itself all resolve to null.
Jest surfaces this as a complaint about whichever module its config mentions first (transform → ts-jest), which is why the error text points at an innocent package.
It is NOT --ignore-scripts (an intermediate suspicion, refuted)
Because the catch-up merge used yarn install --ignore-scripts, the obvious suspect was napi-rs’s napi-postinstall never running. Refuted by re-test: a full yarn install --frozen-lockfilewith scripts enabled still leaves @unrs/resolver-binding-darwin-arm64 absent and require('unrs-resolver') still fails. The postinstall script can’t install a package that ignore-optional never downloaded — the failure is at fetch time, not build time.
Diagnostic one-liner — is it the resolver, or is it the package?
node -e "const R=require('jest-resolve').default; for (const m of ['lodash','jest-resolve']) console.log(m, R.findNodeModule(m,{basedir:process.cwd()}))"
If lodash and jest-resolve both print null, the native resolver binding is missing — it is not a per-package problem, and no amount of reinstalling the named package will help. If they print real paths, the named package genuinely is broken.
Fix — local dev environment only
For Agents
Do not change .yarnrc. The evidence does not justify a repo-wide change: CI is green on the same config (see Why .yarnrc was NOT changed). Install the missing binding into node_modules on the affected macOS machine and move on.
Leaves package.json and yarn.lock untouched — safe when you don’t want to widen a focused PR’s diff:
Use the binding that matches the host (resolver-binding-linux-x64-gnu on the usual Linux boxes) and keep the version aligned with the resolved unrs-resolver in yarn.lock (1.12.2 here).
Result:yarn test:unit runs clean on the merged FKITDEV-8887 branch, against stock dependencies with only this binding added locally — 119 suites passed, 1051 tests passed, 47 skipped, 0 failures.package.json and yarn.lock are untouched by any of this.
Why .yarnrc was NOT changed
Dropping or narrowing --install.ignore-optional true was the original recommendation. It is not justified: the flag predates Jest 30 and is arguably stale, but the only observed breakage is on one macOS/arm64 workstation while CI — which honours the same .yarnrc — is green. Changing a repo-wide install flag to fix one machine is the wrong trade. Revisit only if a second environment (or CI) actually breaks.
Why is CI green? — ANSWERED (partially)
Answer: CI is green. .github/workflows/pull-request.yaml installs with yarn install --frozen-lockfile — the same .yarnrc, so ignore-optional applies there too — and all 7 checks pass on the FKITDEV-8887 head commit, including Unit Tests.
Still open:why the Linux self-hosted runners resolve the native binding when macOS/arm64 does not. Untested hypotheses (do not assert any of these as fact): a warm/cached node_modules on the self-hosted runner that predates the flag; @unrs/resolver-binding-linux-x64-gnu arriving through a different path than the darwin one; unrs-resolver having a usable fallback on glibc-linux; or the runner image shipping the binding globally. Worth a ls node_modules/@unrs on a runner if anyone gets shell access.
The real secondary lesson — a Yarn v6 cache entry can be silently corrupt
This is what the retracted “broken ts-jest publish” section was actually looking at.
Symptom: an installed package whose dist/ (or other built output) is simply missing, reproducing across repeated yarn install runs and across worktrees — which makes it look like a bad upstream publish.
Reality: the defect lived only in the local Yarn Classic cache at
Yarn happily reuses a cache entry indefinitely once it is written; nothing re-validates its contents on later installs, so one bad fetch poisons every subsequent install on that machine.
Fix:
yarn cache clean ts-jest # scope to the suspect packageyarn install --frozen-lockfile
After that, node_modules/ts-jest/dist/index.js exists and ts-jest@29.4.11 works — the full unit suite passes against stock 29.4.11.
Tell-tale: a CI log file inside a node_modules package
The corrupt directory contained npm-view.err — the publisher’s own npm E404 output with paths under /home/runner/.npm. A stray build/CI log (*.err, *.log, npm-debug.log) sitting inside an installed package is evidence of a bad local cache artifact, not proof of a bad registry publish. Before blaming upstream, verify against the registry directly (npm pack <pkg>@<version> into a temp dir and inspect that tarball) — a cache-sourced directory proves nothing about what was published.
Reusable lesson
A Jest Module X ... was not found error names a victim, not a culprit. Before touching the named package, ask whether the resolver itself is dead:
Run the findNodeModule one-liner above on two known-good packages.
All null → resolver/native-binding problem (check .yarnrc, .npmrc, --ignore-optional, --no-optional, offline mirrors, platform mismatch).
Real paths → then, and only then, inspect the named package on disk (ls node_modules/<pkg> for a missing dist/).
If the package on disk is damaged, suspect the local cache before the registry: yarn cache clean <pkg> + reinstall. Only if it survives a clean-cache reinstall is it an upstream problem — confirm by inspecting a freshly npm packed tarball, not the installed directory.
More generally:
ignore-optional is not a safe blanket setting once any dependency uses napi-rs-style per-platform optional bindings. Same trap class applies to esbuild, swc, rollup, lightningcss, sharp.
A local-only reproduction is not a repo bug. Check CI before proposing a repo-wide config change; “it fails on my machine and CI is green” bounds the fix to your machine.
Related
FKITDEV-8887 — where this was hit (PR #3066 devel catch-up merge)
vuer_css — the affected repo (.yarnrc--install.ignore-optional true)