When a dependency (my-adapter) has both:
- A runtime dependency on a package (
some-lib) - 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'
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.
The module fallback service handles requests from workerd when it can't resolve an import natively. The flow is:
-
my-adapter/dist/index.jscontainsimport { createApp } from "some-lib"(a bare specifier left by tsup, sincesome-libis a runtime dependency and tsup externalizes dependencies by default). -
When workerd encounters this import, it sends a fallback request. The
handleModuleFallbackRequestfunction inmodule-fallback.tsreceives: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-libreferrer:.../my-adapter/dist/index.js
-
getApproximateSpecifier(target, referrerDir)computesspecifier = "some-lib"from the target. -
The
resolve()function callsviteResolve(vite, specifier, referrer), which callsvite.pluginContainer.resolveId("some-lib", ".../my-adapter/dist/index.js"). -
Here's the bug: Vite's resolver, given specifier
"some-lib"relative to a file insidemy-adapter, finds thatmy-adapter/package.jsonhas an exports map entry"./some-lib"and resolves it tomy-adapter/dist/some-lib.jsinstead of thesome-libnpm package innode_modules. -
Since
target !== filePath, theload()function returns a redirect tomy-adapter/dist/some-lib.js. -
workerd follows the redirect and loads
my-adapter/dist/some-lib.js— the compatibility wrapper file, which does NOT exportcreateApp. Hence:SyntaxError: The requested module 'some-lib' does not provide an export named 'createApp'.
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.
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.
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".
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 runThe test passes — import { createApp } from 'some-lib' inside my-adapter/dist/index.js should resolve to the some-lib npm package.
FAIL src/index.test.ts [ src/index.test.ts ]
SyntaxError: The requested module 'some-lib' does not provide an export named 'createApp'
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.
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.
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'],
},
},
},
},@cloudflare/vitest-pool-workers: 0.12.14vitest: 3.2.4wrangler: 3.114.17pnpm: 10.x- Node.js: 24.x