How to Fix: turbo fails to resolve with certain node import subpaths
Turbo mode is failing here because the app resolves a Node.js package import subpath differently under the Turbopack pipeline than it does under the standard dev server. In plain terms: pnpm dev succeeds, but pnpm dev --turbo hits a resolver gap when a package uses modern exports/imports subpath mapping that Turbopack does not fully interpret the same way.
Table of Contents
The reproduction linked in this repository demonstrates a classic compatibility mismatch between package.json subpath imports and Turbopack module resolution. The good news is that the issue is usually fixable by changing how internal modules are exported and imported, or by temporarily avoiding unsupported subpath patterns in the turbo build path.
Understanding the Root Cause
This bug appears when code relies on Node import subpaths, typically declared in package.json using fields such as exports or imports. These features let a package expose paths like my-package/utils or internal aliases like #utils instead of requiring deep relative imports.
Under the normal development server, the resolver chain often passes through tooling that already handles these mappings correctly. Under --turbo, however, resolution is delegated to Turbopack, which may not fully support the same subpath semantics in the exact package layout being used.
Technically, the failure usually comes from one of these patterns:
- Subpath aliases defined in
importsare valid in Node.js, but not fully recognized by Turbopack in all contexts. - Workspace package exports are declared in a way that Node accepts, but Turbopack cannot resolve consistently during dependency graph creation.
- TypeScript path aliases, Node subpath imports, and package exports are mixed together, but only some of them are understood by the turbo resolver.
- Conditional exports point to files or conditions that the turbo pipeline does not prioritize the same way as Node or the non-turbo dev server.
So the root cause is not that the import path is necessarily invalid. It is that the same import contract is interpreted by two different resolvers, and Turbopack currently falls short on certain subpath cases.
Step-by-Step Solution
The most reliable fix is to replace the problematic subpath pattern with a resolution strategy that both Next.js and Turbopack can resolve today.
1. Inspect the package using subpath imports
Look for a package.json that contains an imports or exports field like this:
{
"name": "@acme/ui",
"imports": {
"#components/*": "./src/components/*"
},
"exports": {
"./button": "./src/button.tsx"
}
}
If your app imports from an internal alias such as #components/button, or from a package subpath that depends on modern mapping behavior, that is the first thing to change.
2. Prefer explicit package exports over internal imports aliases
If the package is consumed externally, expose stable public entry points via exports instead of relying on private imports aliases.
{
"name": "@acme/ui",
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
}
}
Then import them like this:
import { Button } from '@acme/ui/button'
This is usually safer than:
import { Button } from '#components/button'
Why this helps: package-level public subpath exports are more broadly supported across tooling than private Node-only alias patterns.
3. Avoid unresolved private subpaths in app code
If the app currently imports package internals using unsupported aliases, switch to one of these:
- a public package export
- a direct relative path inside the same package
- a TypeScript alias configured consistently across the app
Example refactor:
// Before
import { Button } from '#components/button'
// After
import { Button } from './components/button'
Or, if importing from another workspace package:
// Before
import { Button } from '#ui/button'
// After
import { Button } from '@acme/ui/button'
4. If needed, flatten the package entry structure
Some monorepo packages work better with a simple entrypoint design while Turbopack support matures.
{
"name": "@acme/ui",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
And then re-export internals from a single barrel file:
// src/index.ts
export * from './button'
export * from './card'
Usage:
import { Button, Card } from '@acme/ui'
This avoids deep subpath resolution entirely.
5. Align TypeScript paths only if they are truly needed
If you use tsconfig.json path aliases, make sure they are not masquerading as Node subpath imports. A safer setup looks like this:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
Then import consistently:
import { Button } from '@/components/button'
Do not assume a TypeScript alias automatically behaves like a Node imports alias under every bundler.
6. Clear caches and retest both modes
After changing exports or imports, remove stale build artifacts and retest:
rm -rf .next
rm -rf node_modules/.cache
pnpm dev
pnpm dev --turbo
If the non-turbo mode works but turbo still fails, you are likely still hitting an unsupported resolution branch somewhere in the dependency tree.
7. Temporary fallback if you need immediate stability
If your team cannot refactor imports right away, the practical workaround is to avoid turbo for that package path until support improves.
pnpm dev
That is not the ideal long-term answer, but it is often the right operational choice when developer productivity matters more than testing an experimental resolver path.
Common Edge Cases
- Conditional exports mismatch: If
exportscontains conditions likeimport,require,default, or environment-specific branches, Turbopack may choose differently than expected. - Workspace symlink behavior: In a monorepo, symlinked packages can expose subtle resolver differences, especially when source files are imported directly instead of built artifacts.
- Missing file extensions: Some resolution flows are stricter about
.js,.ts, or.tsxtargets defined in package exports. - Private
#aliasimports: These are valid Node patterns, but support outside pure Node execution can be inconsistent across bundlers. - Barrel file cycles: If you flatten exports into a single index file, watch for circular imports that may create runtime issues unrelated to the original resolver bug.
- Mixed ESM and CJS: A package exposing both module systems may work in one pipeline and break in another if the conditions are ambiguous.
FAQ
Does this mean my package.json configuration is wrong?
Not necessarily. The configuration may be valid for Node.js and still fail under Turbopack because bundler support for subpath resolution is not always feature-complete.
Should I use exports or imports for shared package APIs?
Use exports for public package entry points. Reserve imports mainly for package-internal aliases, and be cautious because tooling support can vary.
What is the most future-proof workaround right now?
The safest approach is to expose explicit public entrypoints through package subpath exports or a single package root export, then import those paths consistently across the monorepo.
In short, the fix is to simplify module boundaries: prefer stable public exports, avoid private subpath aliases where Turbopack is involved, and keep TypeScript aliasing separate from Node package resolution semantics. That removes the resolver ambiguity causing pnpm dev --turbo to fail while standard dev mode still works.