DEV Community

Cover image for Vite+ — Chapter 4: Vite+ at Scale — Monorepos, Tasks, Caching, and CI
Othmane Nemli
Othmane Nemli

Posted on

Vite+ — Chapter 4: Vite+ at Scale — Monorepos, Tasks, Caching, and CI

In the previous chapter, we used Vite+ for a normal application:

vp dev
vp check
vp test
vp build
Enter fullscreen mode Exit fullscreen mode

That workflow is already useful.

But there's another situation where Vite+ becomes much more interesting:

What happens when your project becomes a lot bigger?

Imagine that one application becomes five, then you add shared UI components, then shared utilities, then a design system, then internal libraries.

Eventually, your repository might look like this:

company-project/
├── apps/
│   ├── web/
│   ├── admin/
│   └── docs/
│
├── packages/
│   ├── ui/
│   ├── utils/
│   ├── config/
│   └── api/
│
└── package.json
Enter fullscreen mode Exit fullscreen mode

This is a monorepo.

And this is where running every task all the time can become expensive.

Vite+ includes Vite Task specifically to handle dependency-aware task execution and caching in monorepos.


1. What Is a Monorepo?

Before talking about Vite+, let's understand the problem.

A monorepo is simply a repository containing multiple projects or packages.

For example:

my-company/
│
├── apps/
│   ├── web/
│   └── mobile/
│
└── packages/
    ├── ui/
    ├── utils/
    └── types/
Enter fullscreen mode Exit fullscreen mode

Here we might have:

  • web → the main website
  • mobile → a mobile application
  • ui → shared UI components
  • utils → shared utilities
  • types → shared TypeScript types

The advantage is that everything lives together.

A developer can change a shared package and immediately test the applications that use it.

But this also creates a new problem:

Which tasks actually need to run?


2. The Dependency Problem

Let's say we have:

web
 │
 └── ui
      │
      └── utils
Enter fullscreen mode Exit fullscreen mode

This means:

web depends on ui
ui depends on utils
Enter fullscreen mode Exit fullscreen mode

Now imagine you change something inside:

packages/utils/
Enter fullscreen mode Exit fullscreen mode

Should we rebuild everything? Maybe.

But what if you only changed:

apps/docs/
Enter fullscreen mode Exit fullscreen mode

Do we really need to rebuild web? Probably not.

A large monorepo can contain hundreds of tasks.

Running all of them after every small change wastes time.

This is where a task system becomes important.


3. What Is a Task?

A task is simply something your project needs to do.

For example:

build
test
lint
typecheck
Enter fullscreen mode Exit fullscreen mode

A package might have:

{
  "scripts": {
    "build": "vite build",
    "test": "vitest",
    "lint": "oxlint"
  }
}
Enter fullscreen mode Exit fullscreen mode

These are tasks, in a small application, running them manually isn't difficult.

In a monorepo, however, you might have:

20 packages
×
4 tasks
=
80 possible tasks
Enter fullscreen mode Exit fullscreen mode

Now the task runner has a much bigger job.


4. Enter vp run

Vite+ provides:

vp run
Enter fullscreen mode Exit fullscreen mode

This command can run package scripts and monorepo tasks while understanding dependencies between them. It also supports task caching.

For example:

vp run build
Enter fullscreen mode Exit fullscreen mode

You can think of this as saying:

"Run the build tasks that are relevant to this project."

Instead of manually navigating through every package.


5. Task Dependencies

Let's use a simple example.

Imagine:

packages/utils
       ↓
packages/ui
       ↓
apps/web
Enter fullscreen mode Exit fullscreen mode

The relationships are:

utils
  ↓
ui
  ↓
web
Enter fullscreen mode Exit fullscreen mode

If web needs ui, and ui needs utils, then the build order matters.

You can't necessarily build:

web
Enter fullscreen mode Exit fullscreen mode

before:

ui
Enter fullscreen mode Exit fullscreen mode

and you can't build:

ui
Enter fullscreen mode Exit fullscreen mode

before:

utils
Enter fullscreen mode Exit fullscreen mode

The dependency graph is therefore:

utils
  │
  ▼
 ui
  │
  ▼
web
Enter fullscreen mode Exit fullscreen mode

A task runner can use this graph to determine what needs to happen first.


6. Why Dependency-Aware Execution Matters

Imagine your repository contains:

apps/
├── web
├── admin
└── docs

packages/
├── ui
├── utils
├── api
└── config
Enter fullscreen mode Exit fullscreen mode

Suppose:

web → ui → utils
admin → ui → utils
docs → ui
Enter fullscreen mode Exit fullscreen mode

If utils changes, both web and admin may be affected.

But docs might not depend directly on utils.

A dependency-aware task runner can use those relationships instead of blindly running everything.

Conceptually:

utils changed
     │
     ├── ui
     │    ├── web
     │    └── admin
     │
     └── unrelated packages
          ↓
        skip
Enter fullscreen mode Exit fullscreen mode

That's the important idea.


7. Caching

Now we reach one of the most useful features.

Imagine you run:

vp run build # takes 45 seconds
Enter fullscreen mode Exit fullscreen mode

Then you run the exact same command again.

If nothing relevant changed, rebuilding everything from scratch is unnecessary.

This is where caching comes in.

Conceptually:

First run

Source code
    ↓
Build
    ↓
45 seconds
    ↓
Save result
Enter fullscreen mode Exit fullscreen mode

Then:

Second run

Same inputs
    ↓
Cache lookup
    ↓
Reuse previous result
Enter fullscreen mode Exit fullscreen mode

Instead of "45 seconds", you can potentially get a result much faster.

Vite+ describes vp run as providing caching and dependency-aware scheduling for monorepo tasks.


8. What Does the Cache Actually Mean?

A common misunderstanding is:

"The cache means Vite+ never runs the command again."

That's not quite right.

The important question is whether the task's relevant inputs have changed.

Think about:

Input
 ↓
Task
 ↓
Output
Enter fullscreen mode Exit fullscreen mode

For example:

src/
package.json
tsconfig.json
environment
     ↓
   build
     ↓
   dist/
Enter fullscreen mode Exit fullscreen mode

If the inputs are unchanged, the previous result may be reusable.

If an important input changes:

src/Button.tsx
Enter fullscreen mode Exit fullscreen mode

then the task may need to run again.

Conceptually:

Same inputs
    ↓
Cache hit
    ↓
Reuse result
Enter fullscreen mode Exit fullscreen mode

versus:

Changed inputs
    ↓
Cache miss
    ↓
Run task again
Enter fullscreen mode Exit fullscreen mode

This is the basic idea behind task caching.


9. Cache Hits and Cache Misses

You'll often hear two terms:

Cache hit

The previous result can be reused.

web#build — cache hit
Enter fullscreen mode Exit fullscreen mode

Cache miss

The task needs to run again.

web#build — cache miss
Enter fullscreen mode Exit fullscreen mode

For example:

First run:

utils#build   → cache miss
ui#build      → cache miss
web#build     → cache miss
Enter fullscreen mode Exit fullscreen mode

Everything has to run.

Then you run it again without changing anything:

Second run:

utils#build   → cache hit
ui#build      → cache hit
web#build     → cache hit
Enter fullscreen mode Exit fullscreen mode

The exact behavior depends on the task inputs and configuration, but this is the mental model developers should have.


10. Why This Matters in CI

Now let's move from your laptop to CI.

A typical CI pipeline might look like:

Developer pushes code
        ↓
     GitHub
        ↓
     CI starts
        ↓
    Install deps
        ↓
      Check
        ↓
      Test
        ↓
      Build
Enter fullscreen mode Exit fullscreen mode

Without caching, every CI run may repeat expensive work.

Imagine:

Install     → 30 sec
Lint        → 20 sec
Tests       → 60 sec
Build       → 90 sec
Enter fullscreen mode Exit fullscreen mode

That's already several minutes.

Now imagine a repository with dozens of packages, the wasted time adds up quickly.


11. Vite+ and GitHub Actions

Vite+ provides an official GitHub Action called setup-vp for installing Vite+ in GitHub Actions. The current repository documentation recommends pinning the action to an exact release or commit rather than using the old floating v1 tag.

A simplified example looks like this:

name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: voidzero-dev/setup-vp@<setup-vp-version>
        with:
          node-version: '22'
          cache: true

      - run: vp install
      - run: vp check
      - run: vp test
      - run: vp build
Enter fullscreen mode Exit fullscreen mode

The important part isn't the exact YAML,, it's the workflow:

GitHub Actions
      ↓
   setup-vp
      ↓
   vp install
      ↓
   vp check
      ↓
   vp test
      ↓
   vp build
Enter fullscreen mode Exit fullscreen mode

The same vp commands you use locally can be used in CI. That consistency is valuable.


12. Local Development and CI Use the Same Interface

One thing I particularly like about this approach is that developers don't have to learn a completely different system for CI.

Locally:

vp check
vp test
vp build
Enter fullscreen mode Exit fullscreen mode

CI:

- run: vp check
- run: vp test
- run: vp build
Enter fullscreen mode Exit fullscreen mode

The commands are the same.

That means when something fails in CI, you can often reproduce the same command locally.

For example:

vp test
Enter fullscreen mode Exit fullscreen mode

Instead of trying to understand:

some-custom-ci-script.sh
Enter fullscreen mode Exit fullscreen mode

that internally calls several other tools.


13. A Monorepo Example

Let's imagine a real project.

acme/
├── apps/
│   ├── storefront/
│   └── dashboard/
│
├── packages/
│   ├── ui/
│   ├── auth/
│   ├── api-client/
│   └── utils/
│
├── package.json
└── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Dependencies might look like:

                 ┌── ui ────────┐
                 │              │
storefront ──────┤              │
                 │              ▼
                 └── api-client
                       │
                       ▼
                     utils
Enter fullscreen mode Exit fullscreen mode

And:

dashboard
    │
    ├── ui
    │
    └── auth
          │
          ▼
        utils
Enter fullscreen mode Exit fullscreen mode

Now imagine you modify:

packages/utils/
Enter fullscreen mode Exit fullscreen mode

Several projects could be affected.

A task runner can use the dependency graph to determine the appropriate execution order.

Instead of manually figuring out:

1. Build utils
2. Build api-client
3. Build ui
4. Build storefront
5. Build auth
6. Build dashboard
Enter fullscreen mode Exit fullscreen mode

you can use the task system to coordinate those tasks.


14. Parallelism

There's another important idea here:

Not every task needs to run sequentially.

Suppose:

storefront
    ↓
   ui

dashboard
    ↓
   auth
Enter fullscreen mode Exit fullscreen mode

The two branches are independent.

Conceptually, they can be processed like this:

          ui ────────> storefront
         /
Start ──
         \
          auth ──────> dashboard
Enter fullscreen mode Exit fullscreen mode

Instead of:

ui
 ↓
storefront
 ↓
auth
 ↓
dashboard
Enter fullscreen mode Exit fullscreen mode

a task system can execute independent work concurrently where appropriate.

This becomes increasingly important as the repository grows.

The goal is simple:

Don't make one task wait for another task if there is no dependency between them.


15. vp run Is More Than npm run

At first, you might look at:

vp run build
Enter fullscreen mode Exit fullscreen mode

and think:

"Isn't that basically the same as npm run build?"

For a simple project, the difference may not feel dramatic.

But in a monorepo, the task system adds concepts such as:

  • dependency-aware execution
  • task graphs
  • caching
  • filtering
  • parallel execution
  • task-level configuration

So:

npm run build
Enter fullscreen mode Exit fullscreen mode

is primarily: Run this script

Whereas:

vp run build
Enter fullscreen mode Exit fullscreen mode

can be thought of as: Run the appropriate build tasks across the project while understanding their relationships and cached results.

That distinction becomes important at scale.


16. Filtering Tasks

Large monorepos don't always need everything to run.

Sometimes you only want to work with one package.

For example, you might want to run a task for:

apps/storefront
Enter fullscreen mode Exit fullscreen mode

rather than the entire repository.

Vite+ supports filtering in its task workflow, for example:

vp run --filter storefront build
Enter fullscreen mode Exit fullscreen mode

The exact filters you use depend on your workspace structure and task configuration.

The important idea is:

Whole repository
       ↓
     filter
       ↓
Relevant packages
       ↓
Relevant tasks
Enter fullscreen mode Exit fullscreen mode

This is particularly useful when working locally.


17. One Configuration for the Whole Toolchain

We saw in Chapter 3 that Vite+ can use:

vite.config.ts
Enter fullscreen mode Exit fullscreen mode

as a central configuration file.

For a larger repository, that becomes even more useful.

The configuration can include:

import { defineConfig } from 'vite-plus'

export default defineConfig({
  plugins: [],

  test: {
    include: ['src/**/*.test.ts'],
  },

  lint: {
    ignorePatterns: ['dist/**'],
  },

  fmt: {
    semi: true,
    singleQuote: true,
  },

  run: {
    tasks: {
      'generate:icons': {
        command: 'node scripts/generate-icons.js',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

This means the same configuration surface can describe parts of:

Development
     ↓
Testing
     ↓
Linting
     ↓
Formatting
     ↓
Task execution
Enter fullscreen mode Exit fullscreen mode

The current Vite+ repository documents this unified configuration approach.


18. But Caching Isn't Magic

This is important.

Caching sounds simple:

Nothing changed
    ↓
Use cache
Enter fullscreen mode Exit fullscreen mode

Real projects are more complicated.

A task can read files you didn't expect.

A build can generate files.

A tool can modify something inside node_modules.

Environment variables can affect output.

Generated files can become inputs to another task.

This means cache correctness depends on correctly understanding the task's inputs and outputs.

The Vite+ project has continued improving task-cache reliability, and its release history includes fixes around automatically tracked inputs and caching behavior.

So when introducing caching to a large repository, you should still verify that tasks are configured correctly.


19. Don't Treat Cache Hits as Guaranteed

This is another useful lesson.

You might expect:

vp run build
Enter fullscreen mode Exit fullscreen mode

followed by:

vp run build
Enter fullscreen mode Exit fullscreen mode

to always produce 100% cache hits.

But real-world projects can have tasks that modify their own inputs or produce files that affect the task fingerprint.

The Vite+ issue tracker has documented examples of cache misses caused by generated files and dependency/runtime cache directories.

This doesn't mean caching isn't useful.

It means developers should understand what the cache is actually tracking.

A good mental model is:

Caching is a correctness feature first and a performance feature second.

If the system cannot prove that the previous result is still valid, rerunning the task is safer than incorrectly reusing an old result.


20. A Practical CI Pipeline

For a monorepo, you might eventually have something like:

                 Pull Request
                      │
                      ▼
                 Install deps
                      │
                      ▼
                   vp check
                      │
                      ▼
                   vp test
                      │
                      ▼
                  vp run build
                      │
                      ▼
                Deploy / Publish
Enter fullscreen mode Exit fullscreen mode

The important thing is that developers and CI share the same vocabulary.

Local:

vp check
vp test
vp run build
Enter fullscreen mode Exit fullscreen mode

CI:

- run: vp check
- run: vp test
- run: vp run build
Enter fullscreen mode Exit fullscreen mode

That reduces the gap between:

"It works on my machine."

and:

"Why does CI use completely different commands?"


21. When Does This Actually Matter?

If you're building a small personal website:

1 app
5 components
1 package
Enter fullscreen mode Exit fullscreen mode

you probably won't notice a huge difference.

You can happily use:

vp dev
vp test
vp build
Enter fullscreen mode Exit fullscreen mode

and move on.

But imagine:

10 applications
30 packages
100 developers
hundreds of tasks
Enter fullscreen mode Exit fullscreen mode

Now task execution becomes a real engineering problem.

You care about:

  • What changed?
  • What needs rebuilding?
  • What can run in parallel?
  • What can use a previous result?
  • What should CI execute?
  • How can local and CI workflows stay consistent?

That's the environment where Vite Task and vp run become much more important.


22. The Big Picture

Let's compare the two situations.

Small application

Developer
   │
   ├── vp dev
   ├── vp check
   ├── vp test
   └── vp build
Enter fullscreen mode Exit fullscreen mode

Simple.

Large monorepo

                         Vite+
                           │
                    ┌──────┴──────┐
                    │             │
                  Local           CI
                    │             │
                  vp run        vp run
                    │             │
              ┌─────┴─────┐   ┌──┴───┐
              │           │   │      │
            Tasks      Cache  Tasks  Cache
              │           │   │      │
              └─────┬─────┘   └──┬───┘
                    │             │
                    └──────┬──────┘
                           │
                    Dependency graph
Enter fullscreen mode Exit fullscreen mode

This is where Vite+ starts looking less like:

"A nicer command for Vite."

and more like:

A unified development and task-execution environment for a modern JavaScript repository.


23. Ssummary

In this chapter, we moved from a single application to large repositories, the main concepts were:

Monorepos

Multiple applications and packages living in one repository.

Tasks

Operations such as:

build
test
lint
typecheck
Enter fullscreen mode Exit fullscreen mode

Dependency graphs

Understanding which packages depend on which other packages.

Caching

Reusing previous task results when the relevant inputs haven't changed.

Parallel execution

Running independent tasks at the same time.

CI

Using the same vp workflow locally and in GitHub Actions.

The important command to remember is:

vp run
Enter fullscreen mode Exit fullscreen mode

For a small project, you might barely notice it.

For a large monorepo, it can become one of the most important parts of the Vite+ workflow.


What comes next ?

We've now covered a lot of things, that leaves one big question:

Should you actually use Vite+?

It's easy to get excited about a new toolchain, but every tool has trade-offs.

In next chapter; we'll look at:

  • When Vite+ makes sense
  • When it might be unnecessary
  • Migrating an existing project
  • The benefits and trade-offs
  • The beta/early-stage considerations
  • How to decide whether Vite+ fits your project
  • What to consider before adopting it in a team

That will bring the series together and give you a practical answer to the question:

"Should I use Vite+?"

Top comments (0)