diff --git a/docs/site/content/docs/guides/tools/vitest.mdx b/docs/site/content/docs/guides/tools/vitest.mdx index 43eeb32d9729f..927e914bdb784 100644 --- a/docs/site/content/docs/guides/tools/vitest.mdx +++ b/docs/site/content/docs/guides/tools/vitest.mdx @@ -10,12 +10,16 @@ import { Tab, Tabs } from '#components/tabs'; [Vitest](https://vitest.dev/) is a test runner from the Vite ecosystem. Integrating it with Turborepo will lead to enormous speed-ups. -[The Vitest documentation](https://vitest.dev/guide/workspace) shows how to create a "Vitest Workspace" that runs all tests in the monorepo from one root command, enabling behavior like merged coverage reports out-of-the-box. This feature doesn't follow modern best practices for monorepos, since its designed for compatibility with Jest (whose Workspace feature was built before [package manager Workspaces](/docs/crafting-your-repository/structuring-a-repository)). +[The Vitest documentation](https://vitest.dev/guide/workspace) shows how to create a "Vitest Projects" configuration that runs all tests in the monorepo from one root command, enabling behavior like merged coverage reports out-of-the-box. This feature doesn't follow modern best practices for monorepos, since its designed for compatibility with Jest (whose Workspace feature was built before [package manager Workspaces](/docs/crafting-your-repository/structuring-a-repository)). + + + Vitest has deprecated workspaces in favor of projects. When using projects, individual project vitest configs can't extend the root config anymore since they would inherit the projects configuration. Instead, a separate shared file like `vitest.shared.ts` is needed. + Because of this you have two options, each with their own tradeoffs: - [Leveraging Turborepo for caching](#leveraging-turborepo-for-caching) -- [Using Vitest's Workspace feature](#using-vitests-workspace-feature) +- [Using Vitest's Projects feature](#using-vitests-projects-feature) ### Leveraging Turborepo for caching @@ -143,7 +147,7 @@ turbo run test:watch #### Creating merged coverage reports -[Vitest's Workspace feature](#using-vitests-workspace-feature) creates an out-of-the-box coverage report that merges all of your packages' tests coverage reports. Following the Turborepo strategy, though, you'll have to merge the coverage reports yourself. +[Vitest's Projects feature](#using-vitests-projects-feature) creates an out-of-the-box coverage report that merges all of your packages' tests coverage reports. Following the Turborepo strategy, though, you'll have to merge the coverage reports yourself. The [`with-vitest` @@ -189,13 +193,13 @@ With this in place, run `turbo test && turbo report` to create a merged coverage started with it quickly using `npx create-turbo@latest --example with-vitest`. -### Using Vitest's Workspace feature +### Using Vitest's Projects feature -The Vitest Workspace feature doesn't follow the same model as a [package manager Workspace](/docs/crafting-your-repository/structuring-a-repository). Instead, it uses a root script that then reaches out into each package in the repository to handle the tests in that respective package. +The Vitest Projects feature doesn't follow the same model as a [package manager Workspace](/docs/crafting-your-repository/structuring-a-repository). Instead, it uses a root script that then reaches out into each package in the repository to handle the tests in that respective package. In this model, there aren't package boundaries, from a modern JavaScript ecosystem perspective. This means you can't rely on Turborepo's caching, since Turborepo leans on those package boundaries. -Because of this, you'll need to use [Root Tasks](/docs/crafting-your-repository/configuring-tasks#registering-root-tasks) if you want to run the tests using Turborepo. Once you've configured [a Vitest Workspace](https://vitest.dev/guide/workspace), create the Root Tasks for Turborepo: +Because of this, you'll need to use [Root Tasks](/docs/crafting-your-repository/configuring-tasks#registering-root-tasks) if you want to run the tests using Turborepo. Once you've configured [a Vitest Projects setup](https://vitest.dev/guide/workspace), create the Root Tasks for Turborepo: ```json title="./turbo.json" { @@ -215,21 +219,120 @@ Because of this, you'll need to use [Root Tasks](/docs/crafting-your-repository/ ### Using a hybrid approach -You can combine the benefits of both approaches by implementing a hybrid solution.This approach unifies local development using Vitest's Workspace approach while preserving Turborepo's caching in CI. This comes at the tradeoff of slightly more configuration and a mixed task running model in the repository. +You can combine the benefits of both approaches by implementing a hybrid solution. This approach unifies local development using Vitest's Projects feature while preserving Turborepo's caching in CI. This comes at the tradeoff of slightly more configuration and a mixed task running model in the repository. + +First, create a shared configuration package since individual projects can't extend the root config when using projects. Create a new package for your shared Vitest configuration: + +```json title="./packages/vitest-config/package.json" +{ + "name": "@repo/vitest-config", + "version": "0.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "dependencies": { + "vitest": "latest" + }, + "devDependencies": { + "@repo/typescript-config": "workspace:*", + "typescript": "latest" + } +} +``` + +```json title="./packages/vitest-config/tsconfig.json" +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] +} +``` -```ts title="./vitest.workspace.ts" -import { defineWorkspace } from 'vitest/config'; +```ts title="./packages/vitest-config/src/index.ts" +export const sharedConfig = { + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + // Other shared configuration + } +}; +``` -export default defineWorkspace(['packages/*']); +Then, create your root Vitest configuration using projects: + +```ts title="./vitest.config.ts" +import { defineConfig } from 'vitest/config'; +import { sharedConfig } from '@repo/vitest-config'; + +export default defineConfig({ + ...sharedConfig, + projects: [ + { + name: 'packages', + root: './packages/*', + test: { + ...sharedConfig.test, + // Project-specific configuration + } + } + ] +}); ``` -In this setup, your packages maintain their individual Vitest configurations and scripts: +In this setup, your packages maintain their individual Vitest configurations that import the shared config. First, install the shared config package: ```json title="./packages/ui/package.json" { "scripts": { "test": "vitest run", "test:watch": "vitest --watch" + }, + "devDependencies": { + "@repo/vitest-config": "workspace:*", + "vitest": "latest" + } +} +``` + +Then create the Vitest configuration: + +```ts title="./packages/ui/vitest.config.ts" +import { defineConfig } from 'vitest/config'; +import { sharedConfig } from '@repo/vitest-config'; + +export default defineConfig({ + ...sharedConfig, + test: { + ...sharedConfig.test, + // Package-specific overrides if needed + } +}); +``` + +Make sure to update your `turbo.json` to include the new configuration package in the dependency graph: + +```json title="./turbo.json" +{ + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "test": { + "dependsOn": ["^test", "@repo/vitest-config#build"] + }, + "test:watch": { + "cache": false, + "persistent": true + } } } ``` @@ -239,10 +342,10 @@ While your root `package.json` includes scripts for running tests globally: ```json title="./package.json" { "scripts": { - "test:workspace": "turbo run test", - "test:workspace:watch": "vitest --watch" + "test:projects": "turbo run test", + "test:projects:watch": "vitest --watch" } } ``` -This configuration allows developers to run `pnpm test:workspace:watch` at the root for a seamless local development experience, while CI continues to use `turbo run test` to leverage package-level caching. **You'll still need to handle merged coverage reports manually as described in the previous section**. +This configuration allows developers to run `pnpm test:projects:watch` at the root for a seamless local development experience using Vitest projects, while CI continues to use `turbo run test` to leverage package-level caching. **You'll still need to handle merged coverage reports manually as described in the previous section**. diff --git a/examples/with-vitest/README.md b/examples/with-vitest/README.md index 7d3c4bea05476..b8be0eb4c8fe3 100644 --- a/examples/with-vitest/README.md +++ b/examples/with-vitest/README.md @@ -6,10 +6,35 @@ This Turborepo starter is maintained by the Turborepo core team. This example is based on the `basic` example (`npx create-turbo@latest`) to demonstrate how to use Vitest and get the most out of Turborepo's caching. -For this reason, the only commands in the root package.json are `turbo run test` and `turbo run view-report`. +This example demonstrates two approaches to Vitest configuration: -`turbo run test`: Runs the test in each package using Turborepo. -`turbo run view-report`: Collects coverage from each package and shows it in a merged report. +1. **Package-level caching (Recommended)**: Each package has its own Vitest configuration that imports shared settings from `@repo/vitest-config`. This approach leverages Turborepo's caching effectively. + +2. **Vitest Projects**: A root `vitest.config.ts` uses Vitest's projects feature for unified test running during development. + +## Getting Started + +First, install dependencies and build the shared configuration package: + +```bash +pnpm install +pnpm build --filter=@repo/vitest-config +``` + +## Available Commands + +- `pnpm test`: Runs tests in each package using Turborepo (leverages caching) +- `pnpm test:projects`: Same as above, explicitly named for the package-level approach +- `pnpm test:projects:watch`: Runs tests using Vitest's projects feature in watch mode +- `pnpm view-report`: Collects coverage from each package and shows it in a merged report + +## Configuration Structure + +The example uses a shared `@repo/vitest-config` package that exports: + +- `sharedConfig`: Base configuration with coverage settings +- `baseConfig`: For Node.js packages (like `math`) +- `uiConfig`: For packages requiring jsdom environment (like `web`, `docs`) ### Remote Caching diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 8331d47434f0e..0354441262265 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -3,12 +3,16 @@ "private": true, "scripts": { "test": "turbo run test", + "test:projects": "turbo run test", + "test:projects:watch": "vitest --watch", "view-report": "turbo run view-report" }, "devDependencies": { + "@repo/vitest-config": "workspace:*", "prettier": "^3.5.0", "turbo": "^2.5.0", - "typescript": "5.7.3" + "typescript": "5.7.3", + "vitest": "^3.0.7" }, "packageManager": "pnpm@9.0.0", "engines": { diff --git a/examples/with-vitest/packages/math/vitest.config.ts b/examples/with-vitest/packages/math/vitest.config.ts index 879033a74554c..979f41c3a09a6 100644 --- a/examples/with-vitest/packages/math/vitest.config.ts +++ b/examples/with-vitest/packages/math/vitest.config.ts @@ -1,3 +1,10 @@ -import { baseConfig } from "@repo/vitest-config/base"; +import { defineConfig } from 'vitest/config'; +import { sharedConfig } from '@repo/vitest-config'; -export default baseConfig; +export default defineConfig({ + ...sharedConfig, + test: { + ...sharedConfig.test, + // Package-specific overrides if needed + } +}); diff --git a/examples/with-vitest/packages/vitest-config/package.json b/examples/with-vitest/packages/vitest-config/package.json index 108c8e24efb87..6f4d3e0ebea14 100644 --- a/examples/with-vitest/packages/vitest-config/package.json +++ b/examples/with-vitest/packages/vitest-config/package.json @@ -1,17 +1,24 @@ { "name": "@repo/vitest-config", "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { + ".": "./dist/index.js", "./base": "./dist/configs/base-config.js", "./ui": "./dist/configs/ui-config.js" }, "scripts": { "build": "tsc", + "dev": "tsc --watch", "collect-json-reports": "node dist/scripts/collect-json-outputs.js", "merge-json-reports": "nyc merge coverage/raw coverage/merged/merged-coverage.json", "report": "nyc report -t coverage/merged --report-dir coverage/report --reporter=html --exclude-after-remap false", "view-report": "open coverage/report/index.html" }, + "dependencies": { + "vitest": "^3.0.7" + }, "devDependencies": { "@repo/typescript-config": "workspace:*", "@vitest/coverage-istanbul": "^3.0.7", @@ -19,6 +26,6 @@ "glob": "^11.0.1", "jsdom": "^26.0.0", "nyc": "^17.1.0", - "vitest": "^3.0.7" + "typescript": "latest" } } diff --git a/examples/with-vitest/packages/vitest-config/configs/base-config.ts b/examples/with-vitest/packages/vitest-config/src/configs/base-config.ts similarity index 89% rename from examples/with-vitest/packages/vitest-config/configs/base-config.ts rename to examples/with-vitest/packages/vitest-config/src/configs/base-config.ts index af0de7ac6a5dc..918cb41a36a6c 100644 --- a/examples/with-vitest/packages/vitest-config/configs/base-config.ts +++ b/examples/with-vitest/packages/vitest-config/src/configs/base-config.ts @@ -1,5 +1,4 @@ import { defineConfig } from "vitest/config"; -import path from "node:path"; export const baseConfig = defineConfig({ test: { @@ -16,4 +15,4 @@ export const baseConfig = defineConfig({ enabled: true, }, }, -}); +}); \ No newline at end of file diff --git a/examples/with-vitest/packages/vitest-config/configs/ui-config.ts b/examples/with-vitest/packages/vitest-config/src/configs/ui-config.ts similarity index 98% rename from examples/with-vitest/packages/vitest-config/configs/ui-config.ts rename to examples/with-vitest/packages/vitest-config/src/configs/ui-config.ts index b3afdf89916b2..b1b8fbe8805ba 100644 --- a/examples/with-vitest/packages/vitest-config/configs/ui-config.ts +++ b/examples/with-vitest/packages/vitest-config/src/configs/ui-config.ts @@ -8,4 +8,4 @@ export const uiConfig = mergeConfig( environment: "jsdom", }, }) -); +); \ No newline at end of file diff --git a/examples/with-vitest/packages/vitest-config/src/index.ts b/examples/with-vitest/packages/vitest-config/src/index.ts new file mode 100644 index 0000000000000..4b25ef2def89b --- /dev/null +++ b/examples/with-vitest/packages/vitest-config/src/index.ts @@ -0,0 +1,21 @@ +export const sharedConfig = { + test: { + globals: true, + coverage: { + provider: "istanbul" as const, + reporter: [ + [ + "json", + { + file: `../coverage.json`, + }, + ], + ] as const, + enabled: true, + }, + }, +}; + +// Re-export specific configs for backwards compatibility +export { baseConfig } from './configs/base-config.js'; +export { uiConfig } from './configs/ui-config.js'; \ No newline at end of file diff --git a/examples/with-vitest/packages/vitest-config/scripts/collect-json-outputs.ts b/examples/with-vitest/packages/vitest-config/src/scripts/collect-json-outputs.ts similarity index 98% rename from examples/with-vitest/packages/vitest-config/scripts/collect-json-outputs.ts rename to examples/with-vitest/packages/vitest-config/src/scripts/collect-json-outputs.ts index 40f1f6db1daac..3d136ea3cd945 100644 --- a/examples/with-vitest/packages/vitest-config/scripts/collect-json-outputs.ts +++ b/examples/with-vitest/packages/vitest-config/src/scripts/collect-json-outputs.ts @@ -70,4 +70,4 @@ async function collectCoverageFiles() { } // Run the function -collectCoverageFiles(); +collectCoverageFiles(); \ No newline at end of file diff --git a/examples/with-vitest/packages/vitest-config/tsconfig.json b/examples/with-vitest/packages/vitest-config/tsconfig.json index c4b88cba4ba18..35ad0d9dd8785 100644 --- a/examples/with-vitest/packages/vitest-config/tsconfig.json +++ b/examples/with-vitest/packages/vitest-config/tsconfig.json @@ -1,8 +1,9 @@ { - "extends": ["@repo/typescript-config/base.json"], + "extends": "@repo/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "rootDir": "src" }, - "include": ["configs", "scripts"], - "exclude": ["node_modules", "dist"] + "include": ["src"], + "exclude": ["dist", "node_modules"] } diff --git a/examples/with-vitest/vitest.config.ts b/examples/with-vitest/vitest.config.ts new file mode 100644 index 0000000000000..0027165150cb7 --- /dev/null +++ b/examples/with-vitest/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config'; +import { sharedConfig } from '@repo/vitest-config'; + +export default defineConfig({ + ...sharedConfig, + projects: [ + { + name: 'packages', + root: './packages/*', + test: { + ...sharedConfig.test, + // Project-specific configuration for packages + } + }, + { + name: 'apps', + root: './apps/*', + test: { + ...sharedConfig.test, + // Project-specific configuration for apps + environment: 'jsdom', + } + } + ] +}); \ No newline at end of file