DEV Community

Aleksandr Ippatev
Aleksandr Ippatev

Posted on

How Turborepo Builds Its Graph?

How Turborepo Builds Its Graph

Turborepo creates its dependency graph by analyzing the package.json files within your monorepo. It looks for two key things:

  1. dependencies: What external npm packages and, crucially, what other local workspaces a package depends on.
  2. devDependencies: Similar to dependencies, these also define relationships between workspaces.

When you run turbo run build --graph, Turborepo:

  1. Discovers all workspaces.
  2. For each workspace, it reads its package.json.
  3. It draws a dependency edge from a package (e.g., web-app) to any other workspace listed in its dependencies or devDependencies (e.g., ui-components, utils).
  4. It uses this graph to determine the correct order of execution and what can be cached.

Why Lazy (Dynamic) Imports Are Ignored

Lazy loading, typically done with syntax like import('./some-module') or await require('./other-module'), is a runtime concept.

  • Turborepo operates at the build/package level. It runs tasks (like build, test, lint) on entire packages. It doesn't analyze the source code inside those packages to see how modules are imported at runtime.
  • Dynamic imports are resolved by the application bundler (e.g., Webpack, Vite, Rollup) during the application's build process, not by Turborepo's task runner.
  • The dependency graph Turborepo builds is a task-level graph, not a source-code-level graph.

A Practical Example

Imagine this monorepo structure:

my-turborepo/
├── apps/
│   └── next-app/
│       ├── package.json
│       └── pages/
│           └── index.js
└── packages/
    ├── ui/
    │   ├── Button.jsx
    │   └── package.json
    └── utils/
        ├── helpers.js
        └── package.json
Enter fullscreen mode Exit fullscreen mode

apps/next-app/package.json:

{
  "name": "next-app",
  "dependencies": {
    "ui": "workspace:*",
    "utils": "workspace:*"
  }
}
Enter fullscreen mode Exit fullscreen mode

apps/next-app/pages/index.js:

// Static import - Turborepo knows about this via the package.json
import { Button } from 'ui';

// Dynamic import - Invisible to Turborepo's graph
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'));

function HomePage() {
  return (
    <div>
      <Button>Click Me</Button>
      <DynamicComponent />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What Turborepo Sees:
The graph will show next-app -> depends on -> ui and utils. It will ensure the build tasks for ui and utils run before the build task for next-app.

What Turborepo Does NOT See:

  • The dynamic import of HeavyComponent.
  • Any other dynamic import within the source code.

Implications and Best Practices

  1. Correct Caching: This behavior is correct and desired. If you change a dynamically imported module inside the next-app itself (like HeavyComponent.js), you don't want Turborepo to invalidate the cache for the entire ui or utils packages. You only want to rebuild next-app. Your application bundler (Next.js, Vite, etc.) handles the granular caching of those internal source files.

  2. Defining Dependencies Correctly: The takeaway is that you must declare all package-level dependencies in your package.json files. If next-app uses a component from the ui package, even if it's dynamically imported, the ui package must be listed as a dependency. Turborepo relies solely on this declaration.

In summary: Turborepo's graph is built from package.json declarations, not from scanning source code for import statements. Lazy loading is a runtime concern handled by your bundler and is invisible to Turborepo's task scheduling and caching mechanism.

Top comments (0)