Skip to content

marshallswain/vitest-pool-workers-subpath-export-bug

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

Bug Reproduction: Module fallback resolves bare specifier to wrong subpath export

Summary

When a dependency (my-adapter) has both:

  1. A runtime dependency on a package (some-lib)
  2. A subpath export with the same name ("./some-lib")

The @cloudflare/vitest-pool-workers module fallback service incorrectly resolves the bare specifier 'some-lib' to the subpath export file (my-adapter/dist/some-lib.js) instead of the actual some-lib npm package.

This causes: SyntaxError: The requested module 'some-lib' does not provide an export named 'createApp'

Where to file

This bug is in @cloudflare/vitest-pool-workers, which lives in cloudflare/workers-sdk. The relevant source file is packages/vitest-pool-workers/src/pool/module-fallback.ts.

Root cause

The module fallback service handles requests from workerd when it can't resolve an import natively. The flow is:

  1. my-adapter/dist/index.js contains import { createApp } from "some-lib" (a bare specifier left by tsup, since some-lib is a runtime dependency and tsup externalizes dependencies by default).

  2. When workerd encounters this import, it sends a fallback request. The handleModuleFallbackRequest function in module-fallback.ts receives:

    • target: the path workerd resolved — it joins the bare specifier with the referrer's directory, producing something like .../node_modules/.pnpm/my-adapter@.../node_modules/my-adapter/dist/some-lib
    • referrer: .../my-adapter/dist/index.js
  3. getApproximateSpecifier(target, referrerDir) computes specifier = "some-lib" from the target.

  4. The resolve() function calls viteResolve(vite, specifier, referrer), which calls vite.pluginContainer.resolveId("some-lib", ".../my-adapter/dist/index.js").

  5. Here's the bug: Vite's resolver, given specifier "some-lib" relative to a file inside my-adapter, finds that my-adapter/package.json has an exports map entry "./some-lib" and resolves it to my-adapter/dist/some-lib.js instead of the some-lib npm package in node_modules.

  6. Since target !== filePath, the load() function returns a redirect to my-adapter/dist/some-lib.js.

  7. workerd follows the redirect and loads my-adapter/dist/some-lib.js — the compatibility wrapper file, which does NOT export createApp. Hence: SyntaxError: The requested module 'some-lib' does not provide an export named 'createApp'.

Why npm works but pnpm doesn't

With npm's flat node_modules, the target path workerd computes lands in node_modules/some-lib directly, so Vite's resolver finds the actual package. With pnpm's .pnpm store and symlinks, the target path lands inside my-adapter/dist/, and Vite's resolver matches the subpath export first.

Debug logging

To observe this in action, add logging to the installed @cloudflare/vitest-pool-workers/dist/pool/index.mjs in the load() function (around line 7078) and handleModuleFallbackRequest() (around line 7127). Log target, filePath, specifier, and referrer when the specifier matches your package name. You'll see the redirect going to the wrong file.

Repro structure

test-repro/
├── README.md
├── packages/
│   ├── some-lib/              # Simple package: exports createApp()
│   │   ├── package.json       # "type": "module"
│   │   ├── src/index.ts       # export function createApp() { ... }
│   │   └── tsup.config.ts     # Builds to dist/index.js
│   └── my-adapter/            # Package with the name collision
│       ├── package.json       # Has dependency "some-lib" AND subpath export "./some-lib"
│       ├── src/index.ts       # import { createApp } from 'some-lib' (becomes bare specifier in dist)
│       ├── src/some-lib.ts    # The subpath export source (different module)
│       └── tsup.config.ts     # Builds to dist/index.js + dist/some-lib.js
└── worker/
    ├── package.json           # Depends on both some-lib and my-adapter
    ├── wrangler.jsonc         # main: "src/index.ts"
    ├── vitest.config.ts       # Uses defineWorkersConfig
    └── src/
        ├── index.ts           # Worker entry: imports my-adapter
        └── index.test.ts      # Test: imports cloudflare:test + my-adapter

Key files in my-adapter/package.json:

{
  "exports": {
    ".": { "import": "./dist/index.js" },
    "./some-lib": { "import": "./dist/some-lib.js" }
  },
  "dependencies": {
    "some-lib": "file:../some-lib"
  }
}

The collision: the subpath export "./some-lib" has the same name as the dependency "some-lib".

Steps to Reproduce

Requires pnpm — npm's flat node_modules does not trigger this bug.

# 1. Build the local packages
cd packages/some-lib && npm install && npx tsup && cd ../..
cd packages/my-adapter && npm install && npx tsup && cd ../..

# 2. Install worker deps with pnpm
cd worker && pnpm install --ignore-workspace

# 3. Run tests — this will fail
npx vitest run

Expected

The test passes — import { createApp } from 'some-lib' inside my-adapter/dist/index.js should resolve to the some-lib npm package.

Actual

FAIL  src/index.test.ts [ src/index.test.ts ]
SyntaxError: The requested module 'some-lib' does not provide an export named 'createApp'

Real-world impact

We discovered this bug with @wingscodes/d1 (a private package), which has:

  • A dependency on feathers (import { feathers } from 'feathers')
  • A subpath export "./feathers" (a FeathersJS compatibility wrapper)

The bare specifier 'feathers' in the dist output gets resolved to the ./feathers subpath export instead of the feathers npm package, breaking all tests.

Since the @wingscodes packages are private, we created the some-lib / my-adapter test packages in this repro to demonstrate the same issue.

Workaround

Option 1: Rename the subpath export to avoid the collision

In my-adapter/package.json, rename "./some-lib" to something that doesn't match the dependency name (e.g., "./some-lib-compat"). This is what @wingscodes/[email protected] did to fix the issue.

Option 2: Force Vite to pre-bundle the affected packages

Add these to vitest.config.ts:

optimizeDeps: {
  include: ['some-lib'],
},
ssr: {
  noExternal: ['some-lib', 'my-adapter'],
},
test: {
  deps: {
    optimizer: {
      ssr: {
        include: ['my-adapter', 'some-lib'],
      },
    },
  },
},

Environment

  • @cloudflare/vitest-pool-workers: 0.12.14
  • vitest: 3.2.4
  • wrangler: 3.114.17
  • pnpm: 10.x
  • Node.js: 24.x

About

Minimal repro: @cloudflare/vitest-pool-workers resolves bare specifier to wrong subpath export

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors