How to Fix: `Cannot find module ‘unknown’` with absolute imports in `turbo`

5 min read

The error is misleading, but the failure is real: in a Turborepo, dynamic imports combined with absolute import aliases can break module resolution during bundling, producing the cryptic message Cannot find module ‘unknown’. The root problem is usually not that the file does not exist, but that the bundler cannot statically resolve the aliased path across package boundaries the way your TypeScript config suggests it should.

Understanding the Root Cause

In a monorepo powered by turbo, it is common to define path aliases such as @/components or similar entries in tsconfig.json. These aliases help TypeScript understand where files live, but TypeScript path mapping does not automatically guarantee runtime or bundler resolution.

The issue appears most often when all of the following are true:

  • You are using absolute imports via compilerOptions.paths.
  • You are doing a dynamic import() instead of a regular static import.
  • The imported file lives in another app or package in the Turborepo.
  • Your bundler or framework does not fully resolve that alias in the dynamic-import context.

Why does the message become unknown? Because during the dependency graph build, the bundler fails to transform the aliased import into a concrete filesystem path. Instead of pointing to a real module path in the final graph, it reaches an unresolved state and throws a generic runtime resolution failure.

A typical problematic pattern looks like this:

const mod = await import('@/features/some-file');

If @/features/some-file only exists as a TypeScript alias and the consuming app has no matching bundler-level understanding of that alias, the import may type-check but still fail during execution or build.

In short, TypeScript paths are not the same thing as Node.js resolution or bundler resolution. In Turborepo, that distinction matters even more because apps and packages are built separately and then composed.

Step-by-Step Solution

The most reliable fix is to stop depending on app-local absolute aliases for cross-package dynamic imports and instead expose shared code through a real workspace package with a valid package entry point.

1. Move shared import targets into a package

If the file being imported dynamically is shared, place it in a package such as packages/ui or packages/shared.

apps/
  web/
packages/
  shared/
    src/
      features/
        some-file.ts

2. Export the file from that package

Create or update the package manifest.

{
  "name": "@repo/shared",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts",
    "./features/some-file": "./src/features/some-file.ts"
  }
}

Then export from an index file if needed:

export * from './features/some-file';

3. Import from the package name, not from an app alias

Replace this:

const mod = await import('@/features/some-file');

With this:

const mod = await import('@repo/shared/features/some-file');

This works better because package-based imports are understood more consistently by bundlers, workspaces, and runtime resolution.

4. Keep tsconfig paths aligned, but do not rely on them alone

If you use path aliases for editor support, make sure they reflect real package boundaries.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@repo/shared": ["packages/shared/src/index.ts"],
      "@repo/shared/*": ["packages/shared/src/*"]
    }
  }
}

This is helpful for TypeScript, but the critical part is still the real package import path.

5. If the import is local to the same app, prefer relative paths for dynamic imports

If the target module is not meant to be shared across apps, use a relative path instead of an alias.

const mod = await import('../features/some-file');

Relative paths are easier for bundlers to statically analyze in dynamic import scenarios.

6. Restart the dev server and clear cache

Monorepo tooling can cache stale resolution state. After changing aliases, exports, or package structure, clean and restart.

rm -rf .turbo
rm -rf node_modules/.cache
pnpm dev

If you are using npm or yarn, run the equivalent commands for your setup.

7. Validate framework-specific config

If the app is using Next.js, Vite, Webpack, or another bundler, verify that workspace packages are included correctly. For example, in Next.js monorepos you may need:

const nextConfig = {
  transpilePackages: ['@repo/shared']
};

module.exports = nextConfig;

This ensures the package is compiled correctly when consumed by the app.

Best practice: use absolute aliases for code inside a single app, use workspace package imports for shared code, and use relative paths when performing dynamic imports within the same local folder tree.

Common Edge Cases

  • Alias works in static imports but fails in dynamic imports: static imports are easier for the bundler to analyze, while dynamic imports have stricter resolution requirements.
  • TypeScript passes but runtime fails: this means your tsconfig paths are valid for the type checker, but not for the runtime bundler.
  • Importing from another app instead of a package: apps in a Turborepo should generally not import source files directly from each other. Shared code belongs in packages/.
  • Missing exports field: if your package.json does not expose the subpath being imported, the module may still fail even after moving code into a package.
  • Next.js package transpilation issues: if a shared package contains TypeScript or modern syntax, the consuming app may need explicit transpilation settings.
  • Case sensitivity problems: macOS may hide filename casing mistakes that fail later in Linux CI environments.
  • BaseUrl confusion: setting baseUrl can make local imports seem valid in the editor while still being unresolved by the actual build pipeline.

FAQ

Why does the error say unknown instead of the actual path?

Because the bundler fails before it can map the aliased dynamic import to a concrete module path. The unresolved request collapses into a generic module lookup failure.

Can I keep using @/ aliases in a Turborepo?

Yes, but only safely within the boundaries of a single app or package where the bundler configuration matches the alias. For cross-package sharing, use a real workspace package import such as @repo/shared.

What is the safest fix for dynamic imports in a monorepo?

The safest fix is to avoid cross-app alias imports, move shared modules into packages/, expose them through package.json exports, and import them by package name.

Once you treat shared modules as real packages instead of path-mapped shortcuts, the Cannot find module ‘unknown’ error usually disappears because turbo, the bundler, and TypeScript all resolve the same module path consistently.

Leave a Reply

Your email address will not be published. Required fields are marked *