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:
-
dependencies
: What external npm packages and, crucially, what other local workspaces a package depends on. -
devDependencies
: Similar todependencies
, these also define relationships between workspaces.
When you run turbo run build --graph
, Turborepo:
- Discovers all workspaces.
- For each workspace, it reads its
package.json
. - It draws a dependency edge from a package (e.g.,
web-app
) to any other workspace listed in itsdependencies
ordevDependencies
(e.g.,ui-components
,utils
). - 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
apps/next-app/package.json
:
{
"name": "next-app",
"dependencies": {
"ui": "workspace:*",
"utils": "workspace:*"
}
}
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>
);
}
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
Correct Caching: This behavior is correct and desired. If you change a dynamically imported module inside the
next-app
itself (likeHeavyComponent.js
), you don't want Turborepo to invalidate the cache for the entireui
orutils
packages. You only want to rebuildnext-app
. Your application bundler (Next.js, Vite, etc.) handles the granular caching of those internal source files.Defining Dependencies Correctly: The takeaway is that you must declare all package-level dependencies in your
package.json
files. Ifnext-app
uses a component from theui
package, even if it's dynamically imported, theui
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)