Skip to content

[vitest-plugin] Fix resolve.preserveSymlinks with isolated node_modules - #16609

Open
gpanders wants to merge 1 commit into
mainfrom
ganders/CC-9111
Open

gpanders wants to merge 1 commit into
mainfrom
ganders/CC-9111

Conversation

@gpanders

@gpanders gpanders commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

With resolve.preserveSymlinks: true in a pnpm project, tests fail to start with No such module ".../node_modules/vitest/dist/chai". The plugin sends every dependency through Vite, including Vitest itself. With preserveSymlinks, Vite loads each package from its symlink in node_modules. pnpm puts a package's own dependencies only next to the package's real location, so Vite can't find them from the symlink.

With preserveSymlinks enabled:

  • Files under node_modules resolve to their real path, if the real path is also under node_modules. Packages linked from outside node_modules (npm link, file:) keep their symlinked path, so they still find peer dependencies in the project.
  • This applies to both import and require().
  • fs.realpathSync from node:fs finds real paths. Bazel's rules_js patches this function to keep paths inside the sandbox, and it keeps mapped drive letters on Windows.

Projects without preserveSymlinks behave as before. The plugin's resolveId hook now runs before the hooks of other plugins, but it returns the same results.

Related: CC-9111


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: bug fix

Devin Review

@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: efadc13

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Oct 30, 2026
@workers-devprod
workers-devprod requested review from a team and petebacondarwin and removed request for a team October 30, 2026 20:54
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/vitest-plugin-preserve-symlinks.md: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/helpers.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/module-fallback.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/plugin.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/test/module-fallback.test.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/test/preserve-symlinks.test.ts: [@cloudflare/wrangler]
devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

I'm Bonk, and I've done a quick review of your PR.

Adds pnpm-safe symlink canonicalization for Vitest module resolution.

  1. P1: Posted 1 actionable inline suggestion.

github run

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by the review of efadc13d.

This review, of 2743bc8a

✅ Approved — No issues found. The real-path mapping only runs when resolve.preserveSymlinks is on, maps each id once for both import and require(), and is covered by tests.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🚀 Release: ✅ LGTM

The changeset is present and appropriate for a bug fix. Risk: low.


🔒 Security: ✅ LGTM

The change only maps module ids that Vite has already resolved to their real paths under node_modules. It adds no new input, endpoint or secret.


📖 AGENTS.md: ✅ LGTM

No issues found.


📋 Code Quality: ✅ LGTM

The real-path mapping is applied exactly once per resolution: by the pre-order resolveId hook for import, and by the module fallback service for require(). Virtual, relative and cloudflare:test ids are unaffected, query suffixes are kept, and the test mocks were updated for the new config field.


⚡ Performance: ✅ LGTM

The cost is one synchronous realpathSync per resolved id under node_modules, and only when preserveSymlinks is enabled.


📝 Documentation: ✅ LGTM

No issues found.


Reviewed commit: 2743bc8a

@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@16609

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@16609

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@16609

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@16609

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@16609

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@16609

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@16609

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@16609

miniflare

npm i https://pkg.pr.new/miniflare@16609

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@16609

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@16609

@cloudflare/runtime-types

npm i https://pkg.pr.new/@cloudflare/runtime-types@16609

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@16609

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@16609

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@16609

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@16609

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@16609

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@16609

wrangler

npm i https://pkg.pr.new/wrangler@16609

commit: efadc13

With `resolve.preserveSymlinks: true` in a pnpm project, tests fail to
start with `No such module ".../node_modules/vitest/dist/chai"`. The
plugin sends every dependency through Vite, including Vitest itself.
With `preserveSymlinks`, Vite loads each package from its symlink in
`node_modules`. pnpm puts a package's own dependencies only next to the
package's real location, so Vite can't find them from the symlink.

With `preserveSymlinks` enabled:

- Files under `node_modules` resolve to their real path, if the real
  path is also under `node_modules`. Packages linked from outside
  `node_modules` (`npm link`, `file:`) keep their symlinked path, so
  they still find peer dependencies in the project.
- This applies to both `import` and `require()`.
- `fs.realpathSync` from `node:fs` finds real paths. Bazel's rules_js
  patches this function to keep paths inside the sandbox, and it keeps
  mapped drive letters on Windows.

Projects without `preserveSymlinks` behave as before. The plugin's
`resolveId` hook now runs before the hooks of other plugins, but it
returns the same results.

Related: CC-9111

@clanky-the-clanker clanky-the-clanker Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved — No issues found. The real-path mapping runs only when resolve.preserveSymlinks is enabled, it keeps query and hash suffixes, and tests cover it.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🚀 Release: ✅ LGTM

The pull request includes a changeset, which fits a bug fix. Risk: low.


🔒 Security: ✅ LGTM

The change only rewrites module ids that Vite has already resolved. It keeps the rewritten path only when that path is still under node_modules, and it adds no new input, endpoint or secret.


📖 AGENTS.md: ✅ LGTM

No issues found.


📋 Code Quality: ✅ LGTM

Each resolution is mapped to its real path exactly once: by the pre-order resolveId hook for import, and by the module fallback service for require(). realPackagePath keeps ?query and #hash suffixes and, like Vite, first tries a # as part of the file name. It falls back to the original id when the real path leaves node_modules.


⚡ Performance: ✅ LGTM

The new cost is one synchronous realpathSync per resolved id under node_modules, and only when preserveSymlinks is enabled. Vite caches the extra this.resolve call that uses skipSelf.


📝 Documentation: ✅ LGTM

No issues found.


Reviewed commit: efadc13d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants