Skip to content

[vitest-pool-workers] Fix module fallback resolving bare specifiers to wrong subpath export#12615

Open
marshallswain wants to merge 8 commits intocloudflare:mainfrom
marshallswain:fix/vitest-pool-workers-subpath-export-collision
Open

[vitest-pool-workers] Fix module fallback resolving bare specifiers to wrong subpath export#12615
marshallswain wants to merge 8 commits intocloudflare:mainfrom
marshallswain:fix/vitest-pool-workers-subpath-export-collision

Conversation

@marshallswain
Copy link

@marshallswain marshallswain commented Feb 20, 2026

Reproduction here: https://github.com/marshallswain/vitest-pool-workers-subpath-export-bug

Fixes a bug where the module fallback service resolves bare specifiers to wrong subpath exports.

When a dependency has both an npm dependency and a subpath export with the same name (e.g., dependency "some-lib" and subpath export "./some-lib"), the module fallback service could resolve the bare specifier to the subpath export file instead of the actual npm package. This is particularly triggered when using pnpm, whose symlinked node_modules structure causes workerd to join the bare specifier with the referrer directory, and maybeGetTargetFilePath finds the subpath export file before any resolution logic runs.

pnpm layout that triggers the bug

node_modules/
├── my-adapter → .pnpm/[email protected]/node_modules/my-adapter  (symlink)
├── some-lib   → .pnpm/[email protected]/node_modules/some-lib      (symlink)
└── .pnpm/
    ├── [email protected]/
    │   └── node_modules/
    │       ├── my-adapter/
    │       │   ├── package.json    ← exports: { ".": "./dist/index.js", "./some-lib": "./dist/some-lib.js" }
    │       │   └── dist/
    │       │       ├── index.js    ← import { createApp } from "some-lib"  (bare specifier → npm package)
    │       │       └── some-lib.js ← subpath export (WRONG target)
    │       └── some-lib → ../../[email protected]/node_modules/some-lib  (symlink)
    └── [email protected]/
        └── node_modules/
            └── some-lib/
                ├── package.json
                └── index.js        ← the CORRECT target

Root cause

workerd sends a module fallback request with the bare specifier "some-lib" and the referrer path (e.g., .../my-adapter/dist/index.js). The fallback handler joins these to produce a target path like .../my-adapter/dist/some-lib. The maybeGetTargetFilePath() function then tries file extensions and finds .../my-adapter/dist/some-lib.js — which is the subpath export file, not the npm package.

Fix

Added a maybeCorrectSubpathCollision() check that detects when a resolved path lands inside the same package as the referrer (indicating a likely subpath export collision). When detected, it walks node_modules to find the actual npm package, reads its package.json, and resolves the correct entry point through the exports map, module, or main fields.

The check is applied at both resolution paths:

  1. After maybeGetTargetFilePath() returns early (the primary bug path)
  2. After viteResolve() returns (a secondary path where Vite's resolver could also match the subpath export)

References

This fix aligns the module fallback behavior with Node's ESM resolution algorithm specification. Per the spec, when code inside my-adapter contains the bare specifier import { createApp } from "some-lib", Node resolves it through two stages:

  1. PACKAGE_SELF_RESOLVE — checks if the specifier matches the containing package's own name field. Since pjson.name is "my-adapter", not "some-lib", self-resolution returns undefined and does not consult the exports map at all.

  2. PACKAGE_RESOLVE step 10 — walks up through node_modules/ directories to locate a directory named some-lib, then reads that package's package.json and resolves through its exports/main.

The bug was that both maybeGetTargetFilePath() and Vite's resolver were effectively skipping the node_modules walk and matching the specifier "some-lib" against the referrer package's own subpath exports (i.e., the "./some-lib" entry in my-adapter's exports map). Per the spec, a package's exports map is only consulted for a bare specifier when pjson.name === packageName (self-resolve) or after the package is located via the node_modules walk — neither of which should match my-adapter's exports when resolving the specifier "some-lib".

Note on scope: The resolveExportsEntry() helper in this fix is a pragmatic approximation of the spec's PACKAGE_EXPORTS_RESOLVE, not a full implementation. It hardcodes condition preference as import > default > require and does not handle wildcard/pattern exports (e.g., "./*": "./dist/*.js") or additional conditions like "node" or "worker". This is a reasonable trade-off for a fallback handler — the fix only needs to correctly locate the entry point of the actual npm package once the collision is detected, and the common cases (exports with ".", module, main) are covered.


  • 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: This is a bug fix with no public API changes

@marshallswain marshallswain requested a review from a team as a code owner February 20, 2026 02:08
@changeset-bot
Copy link

changeset-bot bot commented Feb 20, 2026

🦋 Changeset detected

Latest commit: b7f2ee6

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

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

devin-ai-integration[bot]

This comment was marked as resolved.

@marshallswain marshallswain force-pushed the fix/vitest-pool-workers-subpath-export-collision branch from fa9f199 to fee25ec Compare February 20, 2026 02:17
devin-ai-integration[bot]

This comment was marked as resolved.

@marshallswain marshallswain force-pushed the fix/vitest-pool-workers-subpath-export-collision branch from fee25ec to 8942d63 Compare February 20, 2026 04:40
devin-ai-integration[bot]

This comment was marked as resolved.

…o wrong subpath export

When a dependency has both an npm dependency and a subpath export with
the same name (e.g. dependency "some-lib" and subpath export
"./some-lib"), the module fallback service incorrectly resolves the
bare specifier to the subpath export file instead of the actual npm
package. This is triggered by pnpm's symlinked node_modules structure,
which causes workerd to join the bare specifier with the referrer
directory, accidentally matching the subpath export file.

The fix detects this collision by checking whether a bare specifier
resolved to a file within the same package as the referrer, and if so,
walks the node_modules tree to find and resolve the actual npm package.
@marshallswain marshallswain force-pushed the fix/vitest-pool-workers-subpath-export-collision branch from 8942d63 to 36da767 Compare February 20, 2026 16:02
@workers-devprod
Copy link
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • packages/vitest-pool-workers/src/pool/module-fallback.ts: [@cloudflare/wrangler]
  • packages/vitest-pool-workers/test/module-fallback.test.ts: [@cloudflare/wrangler]

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 5, 2026

create-cloudflare

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

@cloudflare/kv-asset-handler

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

miniflare

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

@cloudflare/pages-shared

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

@cloudflare/unenv-preset

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

@cloudflare/vite-plugin

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

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@12615

@cloudflare/workers-editor-shared

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

wrangler

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

commit: fea3134

@dario-piotrowicz
Copy link
Member

Important

Note to self and to anyone who reviews this PR, I think that the bug is only present in pnpm, npm is not effected (and I assume neither other PMs are), see: https://github.com/marshallswain/vitest-pool-workers-subpath-export-bug/blob/76551ccfdc412f65ef90470515f5681fe04c66da/README.md?plain=1#L38-L40

devin-ai-integration[bot]

This comment was marked as resolved.

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

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

3 participants