How to Fix: Next.js 15 import alias not working with turbopack
Next.js 15 with Turbopack can fail to resolve import aliases from internal workspace packages even when the same setup works with regular Next.js builds or TypeScript itself. The break usually appears in monorepos where an app imports code from a sibling package using aliases such as @/components or custom paths defined in tsconfig.json. The result is confusing: TypeScript may compile, your editor may autocomplete correctly, but Turbopack throws module resolution errors at runtime or during development.
Table of Contents
The issue reported in the reproduction repository linked in the GitHub issue comes down to a mismatch between how TypeScript path aliases, workspace packages, and Turbopack module resolution interact in Next.js 15. The fix is to stop relying on app-local aliases inside shared packages and make the internal package resolvable as a real package with explicit exports or relative imports.
Understanding the Root Cause
In a monorepo, each package has its own resolution boundary. If package1 contains imports like @/lib/foo, that alias is usually defined in the consuming app’s tsconfig.json, not inside package1 itself. Turbopack resolves modules more strictly than a loose TypeScript editor experience, so it does not automatically assume that aliases defined for my-app should also apply inside code physically located in another package.
That creates a subtle but important distinction:
- TypeScript paths are primarily a compile-time convenience.
- Turbopack needs resolvable module paths at bundling time.
- Internal workspace packages should behave like standalone packages, not like folders that inherit the app’s alias map.
If a shared package depends on aliases that only exist in the app, Turbopack sees unresolved imports. In practice, this commonly happens in setups like:
|- my-app/
| |- tsconfig.json
| |- app/
|
|- package1/
| |- src/
| |- component.tsx
And inside package1/src/component.tsx:
import { something } from '@/lib/something'
If @ is only configured in my-app/tsconfig.json, then package1 is depending on an alias it does not own. That is the root bug.
Another contributing factor is that Next.js 15 + Turbopack is optimized around explicit package boundaries. If you want shared code to be imported reliably, that shared code should expose stable package entry points through package.json exports, or use relative imports internally.
Step-by-Step Solution
The most reliable fix is:
- Remove app-specific aliases from code inside the internal package.
- Use relative imports inside the package itself.
- Expose the package through
package.jsonexports. - Import the package from the app using the package name, not private folder paths.
- If needed, tell Next.js to transpile the workspace package.
1. Fix imports inside the internal package
Replace alias-based imports like this:
import { Button } from '@/components/button'
With relative imports that are valid from inside the package:
import { Button } from './components/button'
Or:
import { Button } from '../components/button'
This ensures package1 is self-contained and does not depend on the app’s alias configuration.
2. Define a proper package entry in package1/package.json
{
"name": "package1",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
Then create package1/src/index.ts:
export * from './component'
This gives Turbopack a clean package entry point.
3. Import from the package name in the Next.js app
In my-app, use:
import { MyComponent } from 'package1'
Avoid reaching into internal source files like:
import { MyComponent } from '../../package1/src/component'
or mixing app aliases with package internals.
4. Enable transpilation for the workspace package
In my-app/next.config.ts or next.config.js:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
transpilePackages: ['package1'],
}
export default nextConfig
This helps Next.js process the internal package correctly when it contains TypeScript or modern syntax.
5. Keep aliases local to the app, not shared package internals
Your app can still use aliases for its own code. Example my-app/tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
That is fine for files inside my-app. The key rule is: do not assume this alias applies inside sibling workspace packages.
6. Optional: give the package its own local alias map
If you really want aliases inside package1, define them in package1/tsconfig.json and ensure the tooling fully supports them. Example:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
However, this can still be brittle with bundlers unless every tool in the pipeline understands that package-local alias configuration. For stability, relative imports are the safest choice inside shared packages.
7. Final working structure
|- my-app/
| |- app/
| |- next.config.ts
| |- package.json
| |- tsconfig.json
|
|- package1/
| |- package.json
| |- tsconfig.json
| |- src/
| |- index.ts
| |- component.tsx
| |- components/
| |- button.tsx
Example internal package file:
// package1/src/component.tsx
import { Button } from './components/button'
export function MyComponent() {
return <Button />
}
Example app usage:
// my-app/app/page.tsx
import { MyComponent } from 'package1'
export default function Page() {
return <MyComponent />
}
If you want to compare against the original reproduction, use the linked repository from the issue and refactor it so the shared package no longer relies on the app alias.
Common Edge Cases
1. The package works with next dev without Turbopack but breaks with Turbopack
This usually means your setup depends on behavior that is tolerated by one pipeline but not by Turbopack’s stricter resolver. The package boundary is still the problem.
2. TypeScript shows no errors, but the app fails at runtime
Editor resolution is not the same as bundler resolution. TypeScript may understand paths, while Turbopack may not apply those paths across package boundaries the way you expect.
3. Importing subpaths from the package fails
If you use imports like package1/components, define them explicitly in exports:
{
"name": "package1",
"exports": {
".": "./src/index.ts",
"./components": "./src/components/index.ts"
}
}
Without explicit exports, subpath imports can fail.
4. The package is symlinked by a workspace manager and resolution is inconsistent
With pnpm, npm workspaces, or Yarn workspaces, symlinked packages are common. This is fine, but only if the package behaves like a real package with valid entry points and self-contained imports.
5. Mixed ESM and CommonJS settings
If the internal package uses a different module format than the app, you can get misleading import errors. Align your package.json, TypeScript module settings, and Next.js expectations.
6. Missing transpilePackages
If the internal package contains uncompiled TypeScript or JSX and Next.js is not set to transpile it, the app may fail even after fixing aliases.
7. Using root-level monorepo tsconfig paths and expecting automatic inheritance
A root config can help TypeScript, but bundlers still need resolvable imports in practice. Do not treat monorepo-wide aliases as a substitute for correct package design.
FAQ
Can I keep using @/ aliases in my Next.js app?
Yes. Use them for files that belong to the app itself. The problem starts when a separate internal package depends on aliases that are only defined in the app.
Why do relative imports fix the issue?
Because relative imports are resolved directly from the package file system location and do not depend on external alias configuration. That makes them reliable for Turbopack and for any consumer of the package.
Is transpilePackages enough by itself?
No. transpilePackages helps Next.js compile workspace packages, but it does not magically repair invalid alias assumptions inside those packages. You still need proper internal imports and clean package exports.
Bottom line: this Next.js 15 Turbopack alias bug is usually not a bug in your alias syntax itself. It is a package boundary resolution problem. Treat each internal package as an independent module, use relative imports within it, expose public entry points through package.json, and let the app consume it by package name. That approach is the most stable fix today.