[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat!: add workspace support (#3103)

authored by

Vladimir and committed by
GitHub
(Apr 9, 2023, 3:12 PM +0200) b9d1a975 ddbda102

+1764 -665
+2 -1
README.md
··· 41 41 - Components testing ([Vue](./examples/vue), [React](./examples/react), [Svelte](./examples/svelte), [Lit](./examples/lit), [Vitesse](./examples/vitesse)) 42 42 - Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)) 43 43 - Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench) 44 + - [Workspace](/guide/workspace) support 44 45 - ESM first, top level await 45 46 - Out-of-box TypeScript / JSX support 46 47 - Filtering, timeouts, concurrent for suite and tests 47 48 48 - > Vitest requires Vite >=v3.0.0 and Node >=v14 49 + > Vitest requires Vite >=v3.0.0 and Node >=v14.18 49 50 50 51 51 52 ```ts
+12 -4
pnpm-lock.yaml
··· 290 290 '@types/react-test-renderer': 17.0.2 291 291 '@vitejs/plugin-react': 3.1.0_vite@4.2.1 292 292 '@vitest/ui': link:../../packages/ui 293 - happy-dom: 9.1.7 293 + happy-dom: 9.1.9 294 294 jsdom: 21.1.1 295 295 react-test-renderer: 17.0.2_react@17.0.2 296 296 vite: 4.2.1 ··· 1089 1089 '@vitejs/plugin-vue': 4.1.0_vite@4.2.1+vue@3.2.47 1090 1090 '@vitest/browser': link:../../packages/browser 1091 1091 '@vue/test-utils': 2.3.2_vue@3.2.47 1092 - happy-dom: 9.1.7 1092 + happy-dom: 9.1.9 1093 1093 vite: 4.2.1 1094 1094 vitest: link:../../packages/vitest 1095 1095 vue: 3.2.47 ··· 1324 1324 vitest: workspace:* 1325 1325 devDependencies: 1326 1326 '@vitest/web-worker': link:../../packages/web-worker 1327 + vitest: link:../../packages/vitest 1328 + 1329 + test/workspaces: 1330 + specifiers: 1331 + jsdom: latest 1332 + vitest: workspace:* 1333 + devDependencies: 1334 + jsdom: 21.1.1 1327 1335 vitest: link:../../packages/vitest 1328 1336 1329 1337 packages: ··· 14627 14635 - encoding 14628 14636 dev: true 14629 14637 14630 - /happy-dom/9.1.7: 14631 - resolution: {integrity: sha512-tLkzW0w9EclIsV75hlCFStJa7CYSEUe+OVU8vK+3wzSvzFeXrGnCujuMcYQAPUXDl1CXoQ2ySaTZcqt3ZBJbSw==} 14638 + /happy-dom/9.1.9: 14639 + resolution: {integrity: sha512-OMbnoknA7iNNG/5fwt1JckCKc53QLLFo2ljzit1pCV9SC1TYwcQj0obq0QUTeqIf2p2skbFG69bo19YoSj/1DA==} 14632 14640 dependencies: 14633 14641 css.escape: 1.5.1 14634 14642 he: 1.2.0
+5 -2
docs/.vitepress/components.d.ts
··· 1 - // generated by unplugin-vue-components 2 - // We suggest you to commit this file into source control 1 + /* eslint-disable */ 2 + /* prettier-ignore */ 3 + // @ts-nocheck 4 + // Generated by unplugin-vue-components 3 5 // Read more: https://github.com/vuejs/core/pull/3399 4 6 import '@vue/runtime-core' 5 7 ··· 12 14 FeaturesList: typeof import('./components/FeaturesList.vue')['default'] 13 15 HomePage: typeof import('./components/HomePage.vue')['default'] 14 16 ListItem: typeof import('./components/ListItem.vue')['default'] 17 + NonProjectOption: typeof import('./components/NonProjectOption.vue')['default'] 15 18 RouterLink: typeof import('vue-router')['RouterLink'] 16 19 RouterView: typeof import('vue-router')['RouterView'] 17 20 }
+4
docs/.vitepress/config.ts
··· 148 148 link: '/guide/features', 149 149 }, 150 150 { 151 + text: 'Workspace', 152 + link: '/guide/workspace', 153 + }, 154 + { 151 155 text: 'CLI', 152 156 link: '/guide/cli', 153 157 },
+5 -5
docs/api/vi.md
··· 251 251 252 252 When `partial` is `true` it will expect a `Partial<T>` as a return value. 253 253 ```ts 254 - import example from './example' 255 - 256 - vi.mock('./example') 254 + import example from './example.js' 255 + 256 + vi.mock('./example.js') 257 257 258 258 test('1+1 equals 2', async () => { 259 259 vi.mocked(example.calc).mockRestore() ··· 271 271 Imports module, bypassing all checks if it should be mocked. Can be useful if you want to mock module partially. 272 272 273 273 ```ts 274 - vi.mock('./example', async () => { 275 - const axios = await vi.importActual('./example') 274 + vi.mock('./example.js', async () => { 275 + const axios = await vi.importActual('./example.js') 276 276 277 277 return { ...axios, get: vi.fn() } 278 278 })
+32 -24
docs/config/index.md
··· 74 74 In addition to the following options, you can also use any configuration option from [Vite](https://vitejs.dev/config/). For example, `define` to define global variables, or `resolve.alias` to define aliases. 75 75 ::: 76 76 77 + ::: tip 78 + All configuration options that are not supported inside a [workspace](/guide/workspace) project config have <NonProjectOption /> sign next them. 79 + ::: 80 + 77 81 ### include 78 82 79 83 - **Type:** `string[]` ··· 99 103 - **Type:** `DepOptimizationConfig & { enabled: boolean }` 100 104 - **Version:** Since Vitest 0.29.0 101 105 - **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html) 106 + 107 + ::: warning 108 + This feature is temporary disabled since Vitest 0.30.0. 109 + ::: 102 110 103 111 Enable dependency optimization. If you have a lot of tests, this might improve their performance. 104 112 ··· 141 149 142 150 This might potentially cause some misalignment if a package has different logic in ESM and CJS mode. 143 151 144 - #### deps.registerNodeLoader 152 + #### deps.registerNodeLoader<NonProjectOption /> 145 153 146 154 - **Type:** `boolean` 147 155 - **Default:** `false` ··· 411 419 }) 412 420 ``` 413 421 414 - ### update 422 + ### update<NonProjectOption /> 415 423 416 424 - **Type:** `boolean` 417 425 - **Default:** `false` ··· 419 427 420 428 Update snapshot files. This will update all changed snapshots and delete obsolete ones. 421 429 422 - ### watch 430 + ### watch<NonProjectOption /> 423 431 424 432 - **Type:** `boolean` 425 433 - **Default:** `true` ··· 434 442 435 443 Project root 436 444 437 - ### reporters 445 + ### reporters<NonProjectOption /> 438 446 439 447 - **Type:** `Reporter | Reporter[]` 440 448 - **Default:** `'default'` ··· 452 460 - `'hanging-process'` - displays a list of hanging processes, if Vitest cannot exit process safely. This might be a heavy operation, enable it only if Vitest consistently cannot exit process 453 461 - path of a custom reporter (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`) 454 462 455 - ### outputFile 463 + ### outputFile<NonProjectOption /> 456 464 457 465 - **Type:** `string | Record<string, string>` 458 466 - **CLI:** `--outputFile=<path>`, `--outputFile.json=./path` ··· 485 493 This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that. 486 494 ::: 487 495 488 - ### maxThreads 496 + ### maxThreads<NonProjectOption /> 489 497 490 498 - **Type:** `number` 491 499 - **Default:** _available CPUs_ 492 500 493 501 Maximum number of threads. You can also use `VITEST_MAX_THREADS` environment variable. 494 502 495 - ### minThreads 503 + ### minThreads<NonProjectOption /> 496 504 497 505 - **Type:** `number` 498 506 - **Default:** _available CPUs_ 499 507 500 508 Minimum number of threads. You can also use `VITEST_MIN_THREADS` environment variable. 501 509 502 - ### useAtomics 510 + ### useAtomics<NonProjectOption /> 503 511 504 512 - **Type:** `boolean` 505 513 - **Default:** `false` ··· 524 532 525 533 Default timeout of a hook in milliseconds 526 534 527 - ### teardownTimeout 535 + ### teardownTimeout<NonProjectOption /> 528 536 529 537 - **Type:** `number` 530 538 - **Default:** `10000` 531 539 532 540 Default timeout to wait for close when Vitest shuts down, in milliseconds 533 541 534 - ### silent 542 + ### silent<NonProjectOption /> 535 543 536 544 - **Type:** `boolean` 537 545 - **Default:** `false` ··· 591 599 ::: 592 600 593 601 594 - ### watchExclude 602 + ### watchExclude<NonProjectOption /> 595 603 596 604 - **Type:** `string[]` 597 605 - **Default:** `['**/node_modules/**', '**/dist/**']` 598 606 599 607 Glob pattern of file paths to be ignored from triggering watch rerun. 600 608 601 - ### forceRerunTriggers 609 + ### forceRerunTriggers<NonProjectOption /> 602 610 603 611 - **Type**: `string[]` 604 612 - **Default:** `['**/package.json/**', '**/vitest.config.*/**', '**/vite.config.*/**']` ··· 626 634 627 635 Isolate environment for each test file. Does not work if you disable [`--threads`](#threads). 628 636 629 - ### coverage 637 + ### coverage<NonProjectOption /> 630 638 631 639 You can use [`c8`](https://github.com/bcoe/c8), [`istanbul`](https://istanbul.js.org/) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. 632 640 ··· 703 711 - **Type:** `boolean` 704 712 - **Default:** `false` 705 713 - **Available for providers:** `'c8' | 'istanbul'` 706 - - **CLI:** `--coverage.all`, --coverage.all=false` 714 + - **CLI:** `--coverage.all`, `--coverage.all=false` 707 715 708 716 Whether to include all files, including the untested ones into report. 709 717 ··· 906 914 907 915 Specifies the module name or path for the custom coverage provider module. See [Guide - Custom Coverage Provider](/guide/coverage#custom-coverage-provider) for more information. 908 916 909 - ### testNamePattern 917 + ### testNamePattern<NonProjectOption /> 910 918 911 919 - **Type** `string | RegExp` 912 920 - **CLI:** `-t <pattern>`, `--testNamePattern=<pattern>`, `--test-name-pattern=<pattern>` ··· 928 936 }) 929 937 ``` 930 938 931 - ### open 939 + ### open<NonProjectOption /> 932 940 933 941 - **Type:** `boolean` 934 942 - **Default:** `false` ··· 1091 1099 }) 1092 1100 ``` 1093 1101 1094 - ### snapshotFormat 1102 + ### snapshotFormat<NonProjectOption /> 1095 1103 1096 1104 - **Type:** `PrettyFormatOptions` 1097 1105 1098 1106 Format options for snapshot testing. These options are passed down to [`pretty-format`](https://www.npmjs.com/package/pretty-format). 1099 1107 1100 - ### resolveSnapshotPath 1108 + ### resolveSnapshotPath<NonProjectOption /> 1101 1109 1102 1110 - **Type**: `(testPath: string, snapExtension: string) => string` 1103 1111 - **Default**: stores snapshot files in `__snapshots__` directory ··· 1122 1130 1123 1131 Allow tests and suites that are marked as only. 1124 1132 1125 - ### dangerouslyIgnoreUnhandledErrors 1133 + ### dangerouslyIgnoreUnhandledErrors<NonProjectOption /> 1126 1134 1127 1135 - **Type**: `boolean` 1128 1136 - **Default**: `false` ··· 1130 1138 1131 1139 Ignore any unhandled errors that occur. 1132 1140 1133 - ### passWithNoTests 1141 + ### passWithNoTests<NonProjectOption /> 1134 1142 1135 1143 - **Type**: `boolean` 1136 1144 - **Default**: `false` ··· 1199 1207 1200 1208 Test above this limit will be queued to run when available slot appears. 1201 1209 1202 - ### cache 1210 + ### cache<NonProjectOption /> 1203 1211 1204 1212 - **Type**: `false | { dir? }` 1205 1213 ··· 1224 1232 npx vitest --sequence.shuffle --sequence.seed=1000 1225 1233 ``` 1226 1234 1227 - #### sequence.sequencer 1235 + #### sequence.sequencer<NonProjectOption /> 1228 1236 1229 1237 - **Type**: `TestSequencerConstructor` 1230 1238 - **Default**: `BaseSequencer` ··· 1243 1251 1244 1252 Vitest usually uses cache to sort tests, so long running tests start earlier - this makes tests run faster. If your tests will run in random order you will lose this performance improvement, but it may be useful to track tests that accidentally depend on another run previously. 1245 1253 1246 - #### sequence.seed 1254 + #### sequence.seed<NonProjectOption /> 1247 1255 1248 1256 - **Type**: `number` 1249 1257 - **Default**: `Date.now()` ··· 1330 1338 1331 1339 Path to custom tsconfig, relative to the project root. 1332 1340 1333 - ### slowTestThreshold 1341 + ### slowTestThreshold<NonProjectOption /> 1334 1342 1335 1343 - **Type**: `number` 1336 1344 - **Default**: `300`
+35 -1
docs/guide/index.md
··· 32 32 ``` 33 33 34 34 :::tip 35 - Vitest requires Vite >=v3.0.0 and Node >=v14 35 + Vitest requires Vite >=v3.0.0 and Node >=v14.18 36 36 ::: 37 37 38 38 It is recommended that you install a copy of `vitest` in your `package.json`, using one of the methods listed above. However, if you would prefer to run `vitest` directly, you can use `npx vitest` (the `npx` command comes with npm and Node.js). ··· 60 60 ``` 61 61 62 62 See the list of config options in the [Config Reference](../config/) 63 + 64 + ## Workspaces Support 65 + 66 + Run different project configurations inside the same project with [Vitest Workspaces](/guide/workspace). You can define a list of files and folders that define you workspace in `vitest.workspace` file. The file supports `js`/`ts`/`json` extensions. This feature works great with monorepo setups. 67 + 68 + ```ts 69 + import { defineWorkspace } from 'vitest/config' 70 + 71 + export default defineWorkspace([ 72 + // you can use a list of glob patterns to define your workspaces 73 + // Vitest expects a list of config files 74 + // or directories where there is a config file 75 + 'packages/*', 76 + 'tests/*/vitest.config.{e2e,unit}.ts', 77 + // you can even run the same tests, 78 + // but with different configs in the same "vitest" process 79 + { 80 + test: { 81 + name: 'happy-dom', 82 + root: './shared_tests', 83 + environment: 'happy-dom', 84 + setupFiles: ['./setup.happy-dom.ts'], 85 + }, 86 + }, 87 + { 88 + test: { 89 + name: 'node', 90 + root: './shared_tests', 91 + environment: 'node', 92 + setupFiles: ['./setup.node.ts'], 93 + }, 94 + }, 95 + ]) 96 + ``` 63 97 64 98 ## Command Line Interface 65 99
+1 -1
docs/guide/mocking.md
··· 216 216 return { Client } 217 217 }) 218 218 219 - vi.mock('./handlers', () => { 219 + vi.mock('./handlers.js', () => { 220 220 return { 221 221 success: vi.fn(), 222 222 failure: vi.fn(),
+137
docs/guide/workspace.md
··· 1 + # Workspace 2 + 3 + Vitest provides built-in support for monorepositories through a workspace configuration file. You can create a workspace to define your project's setups. 4 + 5 + ## Defining a workspace 6 + 7 + A workspace should have a `vitest.workspace` or `vitest.projects` file in its root (in the same folder with your config file, if you have one). Vitest supports `ts`/`js`/`json` extensions for this file. 8 + 9 + Workspace configuration file should have a default export with a list of files or glob patterns referencing your projects. For example, if you have a folder with your projects named `packages`, you can define a workspace with this config file: 10 + 11 + :::code-group 12 + ```ts [vitest.workspace.ts] 13 + export default [ 14 + 'packages/*' 15 + ] 16 + ``` 17 + ::: 18 + 19 + Vitest will consider every folder in `packages` as a separate project even if it doesn't have a config file inside. 20 + 21 + ::: warning 22 + Vitest will not consider the root config as a workspace project (so it will not run tests specified in `include`) unless it is specified in this config. 23 + ::: 24 + 25 + You can also reference projects with their config files: 26 + 27 + :::code-group 28 + ```ts [vitest.workspace.ts] 29 + export default [ 30 + 'packages/*/vitest.config.{e2e,unit}.ts' 31 + ] 32 + ``` 33 + ::: 34 + 35 + This pattern will only include projects with `vitest.config` file that includes `e2e` and `unit` before the extension. 36 + 37 + ::: warning 38 + If you are referencing filenames with glob pattern, make sure your config file starts with `vite.config` or `vitest.config`. Otherwise Vitest will skip it. 39 + ::: 40 + 41 + You can also define projects with inline config. Workspace file supports using both syntaxes at the same time. 42 + 43 + :::code-group 44 + ```ts [vitest.workspace.ts] 45 + import { defineWorkspace } from 'vitest/config' 46 + 47 + // defineWorkspace provides a nice type hinting DX 48 + export default defineWorkspace([ 49 + 'packages/*', 50 + { 51 + // add "extends" to merge two configs together 52 + extends: './vite.config.js', 53 + test: { 54 + include: ['tests/**/*.{browser}.test.{ts,js}'], 55 + // it is recommended to define a name when using inline configs 56 + name: 'happy-dom', 57 + environment: 'happy-dom', 58 + } 59 + }, 60 + { 61 + test: { 62 + include: ['tests/**/*.{node}.test.{ts,js}'], 63 + name: 'node', 64 + environment: 'node', 65 + } 66 + } 67 + ]) 68 + ``` 69 + ::: 70 + 71 + ::: warning 72 + All projects should have unique names. Otherwise Vitest will throw an error. If you do not provide a name inside inline config, Vitest will assign a number. If you don't provide a name inside a project config defined with glob syntax, Vitest will use the directory name by default. 73 + ::: 74 + 75 + If you don't rely on inline configs, you can just create a small json file in your root directory: 76 + 77 + :::code-group 78 + ```json [vitest.workspace.json] 79 + [ 80 + "packages/*" 81 + ] 82 + ``` 83 + ::: 84 + 85 + Workspace projects don't support all configuration properties. For better type safety, use `defineProject` instead of `defineConfig` method inside project configuration files: 86 + 87 + :::code-group 88 + ```ts [packages/a/vitest.config.ts] 89 + import { defineProject } from 'vitest/config' 90 + 91 + export default defineProject({ 92 + test: { 93 + environment: 'jsdom', 94 + // "reporters" is not supported in a project config, 95 + // so it will show an error 96 + reporters: ['json'] 97 + } 98 + }) 99 + ``` 100 + ::: 101 + 102 + ## Configuration 103 + 104 + None of the configuration options are inherited from the root-level config file. You can create a shared config file and merge it with the project config yourself: 105 + 106 + :::code-group 107 + ```ts [packages/a/vitest.config.ts] 108 + import { defineProject, mergeConfig } from 'vitest/config' 109 + import configShared from '../vitest.shared.js' 110 + 111 + export default mergeConfig( 112 + configShared, 113 + defineProject({ 114 + test: { 115 + environment: 'jsdom', 116 + } 117 + }) 118 + ) 119 + ``` 120 + ::: 121 + 122 + Also some of the configuration options are not allowed in a project config. Most notably: 123 + 124 + - `coverage`: coverage is done for the whole workspace 125 + - `reporters`: only root-level reporters can be supported 126 + - `resolveSnapshotPath`: only root-level resolver is respected 127 + - all other options that don't affect test runners 128 + 129 + ::: tip 130 + All configuration options that are not supported inside a project config have <NonProjectOption /> sign next them in ["Config"](/config/) page. 131 + ::: 132 + 133 + ## Coverage 134 + 135 + Coverage for workspace projects works out of the box. But if you have [`all`](/config/#coverage-all) option enabled and use non-conventional extensions in some of you projects, you will need to have a plugin that handles this extension in your root configuration file. 136 + 137 + For example, if you have a package that uses Vue files and it has its own config file, but some of the files are not imported in your tests, coverage will fail trying to analyze the usage of unused files, because it relies on the root configuration rather than project configuration.
+1
packages/coverage-c8/rollup.config.js
··· 17 17 ...builtinModules, 18 18 ...Object.keys(pkg.dependencies || {}), 19 19 ...Object.keys(pkg.peerDependencies || {}), 20 + 'node:inspector', 20 21 'c8/lib/report.js', 21 22 'c8/lib/commands/check-coverage.js', 22 23 'vitest',
+2 -2
test/single-thread/vitest.config.ts
··· 1 1 import { defineConfig } from 'vite' 2 - import { BaseSequencer } from 'vitest/node' 2 + import { BaseSequencer, type WorkspaceSpec } from 'vitest/node' 3 3 4 4 export default defineConfig({ 5 5 test: { 6 6 threads: false, 7 7 sequence: { 8 8 sequencer: class Sequences extends BaseSequencer { 9 - public async sort(files: string[]): Promise<string[]> { 9 + public async sort(files: WorkspaceSpec[]): Promise<WorkspaceSpec[]> { 10 10 return files.sort() 11 11 } 12 12 },
+1 -1
test/watch/vitest.config.ts
··· 6 6 include: ['test/**/*.test.*'], 7 7 8 8 // For Windows CI mostly 9 - testTimeout: 30_000, 9 + testTimeout: process.env.CI ? 30_000 : 10_000, 10 10 11 11 // Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers 12 12 singleThread: true,
+1
test/workspaces/.gitignore
··· 1 + results.json
+21
test/workspaces/globalTest.ts
··· 1 + import { readFile } from 'node:fs/promises' 2 + import assert from 'node:assert/strict' 3 + 4 + export async function teardown() { 5 + const results = JSON.parse(await readFile('./results.json', 'utf-8')) 6 + 7 + try { 8 + assert.ok(results.success) 9 + assert.equal(results.numTotalTestSuites, 6) 10 + assert.equal(results.numTotalTests, 7) 11 + assert.equal(results.numPassedTests, 7) 12 + 13 + const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) 14 + 15 + assert.equal(shared.length, 2) 16 + } 17 + catch (err) { 18 + console.error(err) 19 + process.exit(1) 20 + } 21 + }
+13
test/workspaces/package.json
··· 1 + { 2 + "name": "@vitest/test-workspaces", 3 + "type": "module", 4 + "private": true, 5 + "scripts": { 6 + "test": "vitest", 7 + "coverage": "vitest run --coverage" 8 + }, 9 + "devDependencies": { 10 + "jsdom": "latest", 11 + "vitest": "workspace:*" 12 + } 13 + }
+18
test/workspaces/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + if (process.env.TEST_WATCH) { 4 + // Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks 5 + process.stdin.isTTY = true 6 + process.stdin.setRawMode = () => process.stdin 7 + } 8 + 9 + export default defineConfig({ 10 + test: { 11 + coverage: { 12 + all: true, 13 + }, 14 + reporters: ['default', 'json'], 15 + outputFile: './results.json', 16 + globalSetup: './globalTest.ts', 17 + }, 18 + })
+22
test/workspaces/vitest.workspace.ts
··· 1 + import { defineWorkspace } from 'vitest/config' 2 + 3 + export default defineWorkspace([ 4 + './space_2/*', 5 + './space_*/*.config.ts', 6 + { 7 + test: { 8 + name: 'happy-dom', 9 + root: './space_shared', 10 + environment: 'happy-dom', 11 + setupFiles: ['./setup.jsdom.ts'], 12 + }, 13 + }, 14 + { 15 + test: { 16 + name: 'node', 17 + root: './space_shared', 18 + environment: 'node', 19 + setupFiles: ['./setup.node.ts'], 20 + }, 21 + }, 22 + ])
+1
docs/.vitepress/components/FeaturesList.vue
··· 13 13 <ListItem>Workers multi-threading via <a target="_blank" href="https://github.com/tinylibs/tinypool" rel="noopener noreferrer">Tinypool</a></ListItem> 14 14 <ListItem>Benchmarking support with <a target="_blank" href="https://github.com/tinylibs/tinybench" rel="noopener noreferrer">Tinybench</a></ListItem> 15 15 <ListItem>Filtering, timeouts, concurrent for suite and tests</ListItem> 16 + <ListItem><a href="/guide/workspace">Workspace</a> support</ListItem> 16 17 <ListItem> 17 18 <a href="/guide/snapshot"> 18 19 Jest-compatible Snapshot
+10
docs/.vitepress/components/NonProjectOption.vue
··· 1 + <template> 2 + <span 3 + text-sm 4 + text-orange 5 + cursor-not-allowed 6 + title="Not supported in workspace project config" 7 + > 8 + * 9 + </span> 10 + </template>
+3
docs/.vitepress/style/vars.css
··· 48 48 --vp-button-brand-active-border: var(--vp-c-brand-light); 49 49 --vp-button-brand-active-text: var(--vp-c-text-dark-1); 50 50 --vp-button-brand-active-bg: var(--vp-button-brand-bg); 51 + --vp-code-tab-active-text-color: var(--vp-c-brand); 52 + --vp-code-tab-active-bar-color: var(--vp-c-brand-darker); 53 + --vp-code-tab-divider: var(--vp-c-brand); 51 54 } 52 55 53 56 /**
+1 -1
packages/runner/src/collect.ts
··· 18 18 for (const filepath of paths) { 19 19 const path = relative(config.root, filepath) 20 20 const file: File = { 21 - id: generateHash(path), 21 + id: generateHash(`${path}${config.name || ''}`), 22 22 name: path, 23 23 type: 'suite', 24 24 mode: 'run',
+17
packages/vitest/src/config.ts
··· 1 1 import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite' 2 + import type { ProjectConfig } from './types' 2 3 3 4 export interface UserConfig extends ViteUserConfig { 4 5 test?: ViteUserConfig['test'] 6 + } 7 + 8 + export interface UserWorkspaceConfig extends ViteUserConfig { 9 + extends?: string 10 + test?: ProjectConfig 5 11 } 6 12 7 13 // will import vitest declare test in module 'vite' ··· 12 18 export type UserConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig> 13 19 export type UserConfigExport = UserConfig | Promise<UserConfig> | UserConfigFn 14 20 21 + export type UserProjectConfigFn = (env: ConfigEnv) => UserWorkspaceConfig | Promise<UserWorkspaceConfig> 22 + export type UserProjectConfigExport = UserWorkspaceConfig | Promise<UserWorkspaceConfig> | UserProjectConfigFn 23 + 15 24 export function defineConfig(config: UserConfigExport) { 25 + return config 26 + } 27 + 28 + export function defineProject(config: UserProjectConfigExport) { 29 + return config 30 + } 31 + 32 + export function defineWorkspace(config: (string | UserProjectConfigExport)[]) { 16 33 return config 17 34 }
+31 -13
packages/vitest/src/constants.ts
··· 1 1 // if changed, update also jsdocs and docs 2 2 export const defaultPort = 51204 3 + export const defaultBrowserPort = 63315 3 4 4 5 export const EXIT_CODE_RESTART = 43 5 6 6 7 export const API_PATH = '/__vitest_api__' 7 8 8 - export const configFiles = [ 9 - 'vitest.config.ts', 10 - 'vitest.config.mts', 11 - 'vitest.config.cts', 12 - 'vitest.config.js', 13 - 'vitest.config.mjs', 14 - 'vitest.config.cjs', 15 - 'vite.config.ts', 16 - 'vite.config.mts', 17 - 'vite.config.cts', 18 - 'vite.config.js', 19 - 'vite.config.mjs', 20 - 'vite.config.cjs', 9 + export const CONFIG_NAMES = [ 10 + 'vitest.config', 11 + 'vite.config', 21 12 ] 13 + 14 + const WORKSPACES_NAMES = [ 15 + 'vitest.workspace', 16 + 'vitest.projects', 17 + ] 18 + 19 + const CONFIG_EXTENSIONS = [ 20 + '.ts', 21 + '.mts', 22 + '.cts', 23 + '.js', 24 + '.mjs', 25 + '.cjs', 26 + ] 27 + 28 + export const configFiles = CONFIG_NAMES.flatMap(name => 29 + CONFIG_EXTENSIONS.map(ext => name + ext), 30 + ) 31 + 32 + const WORKSPACES_EXTENSIONS = [ 33 + ...CONFIG_EXTENSIONS, 34 + '.json', 35 + ] 36 + 37 + export const workspacesFiles = WORKSPACES_NAMES.flatMap(name => 38 + WORKSPACES_EXTENSIONS.map(ext => name + ext), 39 + ) 22 40 23 41 export const globalApis = [ 24 42 // suite
+40 -26
test/core/test/sequencers.test.ts
··· 2 2 import { describe, expect, test, vi } from 'vitest' 3 3 import { RandomSequencer } from 'vitest/src/node/sequencers/RandomSequencer' 4 4 import { BaseSequencer } from 'vitest/src/node/sequencers/BaseSequencer' 5 + import type { VitestWorkspace } from 'vitest/node' 6 + import type { WorkspaceSpec } from 'vitest/src/node/pool' 5 7 6 8 function buildCtx() { 7 9 return { ··· 15 17 } as unknown as Vitest 16 18 } 17 19 20 + function buildWorkspace() { 21 + return { 22 + getName: () => 'test', 23 + } as any as VitestWorkspace 24 + } 25 + 26 + const workspace = buildWorkspace() 27 + 28 + function workspaced(files: string[]) { 29 + return files.map(file => [workspace, file] as WorkspaceSpec) 30 + } 31 + 18 32 describe('base sequencer', () => { 19 33 test('sorting when no info is available', async () => { 20 34 const sequencer = new BaseSequencer(buildCtx()) 21 - const files = ['a', 'b', 'c'] 35 + const files = workspaced(['a', 'b', 'c']) 22 36 const sorted = await sequencer.sort(files) 23 37 expect(sorted).toStrictEqual(files) 24 38 }) ··· 26 40 test('prioritize unknown files', async () => { 27 41 const ctx = buildCtx() 28 42 vi.spyOn(ctx.cache, 'getFileStats').mockImplementation((file) => { 29 - if (file === 'b') 43 + if (file === 'test:b') 30 44 return { size: 2 } 31 45 }) 32 46 const sequencer = new BaseSequencer(ctx) 33 - const files = ['b', 'a', 'c'] 47 + const files = workspaced(['b', 'a', 'c']) 34 48 const sorted = await sequencer.sort(files) 35 - expect(sorted).toStrictEqual(['a', 'c', 'b']) 49 + expect(sorted).toStrictEqual(workspaced(['a', 'c', 'b'])) 36 50 }) 37 51 38 52 test('sort by size, larger first', async () => { 39 53 const ctx = buildCtx() 40 54 vi.spyOn(ctx.cache, 'getFileStats').mockImplementation((file) => { 41 - if (file === 'a') 55 + if (file === 'test:a') 42 56 return { size: 1 } 43 - if (file === 'b') 57 + if (file === 'test:b') 44 58 return { size: 2 } 45 - if (file === 'c') 59 + if (file === 'test:c') 46 60 return { size: 3 } 47 61 }) 48 62 const sequencer = new BaseSequencer(ctx) 49 - const files = ['b', 'a', 'c'] 63 + const files = workspaced(['b', 'a', 'c']) 50 64 const sorted = await sequencer.sort(files) 51 - expect(sorted).toStrictEqual(['c', 'b', 'a']) 65 + expect(sorted).toStrictEqual(workspaced(['c', 'b', 'a'])) 52 66 }) 53 67 54 68 test('sort by results, failed first', async () => { 55 69 const ctx = buildCtx() 56 70 vi.spyOn(ctx.cache, 'getFileTestResults').mockImplementation((file) => { 57 - if (file === 'a') 71 + if (file === 'test:a') 58 72 return { failed: false, duration: 1 } 59 - if (file === 'b') 73 + if (file === 'test:b') 60 74 return { failed: true, duration: 1 } 61 - if (file === 'c') 75 + if (file === 'test:c') 62 76 return { failed: true, duration: 1 } 63 77 }) 64 78 const sequencer = new BaseSequencer(ctx) 65 - const files = ['b', 'a', 'c'] 79 + const files = workspaced(['b', 'a', 'c']) 66 80 const sorted = await sequencer.sort(files) 67 - expect(sorted).toStrictEqual(['b', 'c', 'a']) 81 + expect(sorted).toStrictEqual(workspaced(['b', 'c', 'a'])) 68 82 }) 69 83 70 84 test('sort by results, long first', async () => { 71 85 const ctx = buildCtx() 72 86 vi.spyOn(ctx.cache, 'getFileTestResults').mockImplementation((file) => { 73 - if (file === 'a') 87 + if (file === 'test:a') 74 88 return { failed: true, duration: 1 } 75 - if (file === 'b') 89 + if (file === 'test:b') 76 90 return { failed: true, duration: 2 } 77 - if (file === 'c') 91 + if (file === 'test:c') 78 92 return { failed: true, duration: 3 } 79 93 }) 80 94 const sequencer = new BaseSequencer(ctx) 81 - const files = ['b', 'a', 'c'] 95 + const files = workspaced(['b', 'a', 'c']) 82 96 const sorted = await sequencer.sort(files) 83 - expect(sorted).toStrictEqual(['c', 'b', 'a']) 97 + expect(sorted).toStrictEqual(workspaced(['c', 'b', 'a'])) 84 98 }) 85 99 86 100 test('sort by results, long and failed first', async () => { 87 101 const ctx = buildCtx() 88 102 vi.spyOn(ctx.cache, 'getFileTestResults').mockImplementation((file) => { 89 - if (file === 'a') 103 + if (file === 'test:a') 90 104 return { failed: false, duration: 1 } 91 - if (file === 'b') 105 + if (file === 'test:b') 92 106 return { failed: false, duration: 6 } 93 - if (file === 'c') 107 + if (file === 'test:c') 94 108 return { failed: true, duration: 3 } 95 109 }) 96 110 const sequencer = new BaseSequencer(ctx) 97 - const files = ['b', 'a', 'c'] 111 + const files = workspaced(['b', 'a', 'c']) 98 112 const sorted = await sequencer.sort(files) 99 - expect(sorted).toStrictEqual(['c', 'b', 'a']) 113 + expect(sorted).toStrictEqual(workspaced(['c', 'b', 'a'])) 100 114 }) 101 115 }) 102 116 ··· 105 119 const ctx = buildCtx() 106 120 ctx.config.sequence.seed = 101 107 121 const sequencer = new RandomSequencer(ctx) 108 - const files = ['b', 'a', 'c'] 122 + const files = workspaced(['b', 'a', 'c']) 109 123 const sorted = await sequencer.sort(files) 110 - expect(sorted).toStrictEqual(['a', 'c', 'b']) 124 + expect(sorted).toStrictEqual(workspaced(['a', 'c', 'b'])) 111 125 }) 112 126 })
+1 -1
test/reporters/src/context.ts
··· 25 25 } 26 26 27 27 const state: Partial<StateManager> = { 28 - filesMap: new Map<string, File>(), 28 + filesMap: new Map<string, File[]>(), 29 29 } 30 30 31 31 const context: Partial<Vitest> = {
+4
test/reporters/tests/html.test.ts
··· 30 30 const file = resultJson.files[0] 31 31 file.id = 0 32 32 file.collectDuration = 0 33 + file.environmentLoad = 0 34 + file.prepareDuration = 0 33 35 file.setupDuration = 0 34 36 file.result.duration = 0 35 37 file.result.startTime = 0 ··· 62 64 const file = resultJson.files[0] 63 65 file.id = 0 64 66 file.collectDuration = 0 67 + file.environmentLoad = 0 68 + file.prepareDuration = 0 65 69 file.setupDuration = 0 66 70 file.result.duration = 0 67 71 file.result.startTime = 0
+3 -3
test/watch/test/file-watching.test.ts
··· 31 31 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') 32 32 33 33 await vitest.waitForOutput('New code running') 34 - await vitest.waitForOutput('RERUN math.ts') 34 + await vitest.waitForOutput('RERUN ../math.ts') 35 35 await vitest.waitForOutput('1 passed') 36 36 }) 37 37 ··· 41 41 writeFileSync(testFile, editFile(testFileContent), 'utf8') 42 42 43 43 await vitest.waitForOutput('New code running') 44 - await vitest.waitForOutput('RERUN math.test.ts') 44 + await vitest.waitForOutput('RERUN ../math.test.ts') 45 45 await vitest.waitForOutput('1 passed') 46 46 }) 47 47 ··· 71 71 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') 72 72 73 73 await vitest.waitForOutput('New code running') 74 - await vitest.waitForOutput('RERUN math.ts') 74 + await vitest.waitForOutput('RERUN ../math.ts') 75 75 await vitest.waitForOutput('1 passed') 76 76 77 77 vitest.write('q')
+7 -4
test/watch/test/utils.ts
··· 1 1 import { afterEach } from 'vitest' 2 - import { execa } from 'execa' 2 + import { type Options, execa } from 'execa' 3 3 import stripAnsi from 'strip-ansi' 4 4 5 - export async function startWatchMode(...args: string[]) { 6 - const subprocess = execa('vitest', ['--root', 'fixtures', ...args]) 5 + export async function startWatchMode(options?: Options | string, ...args: string[]) { 6 + if (typeof options === 'string') 7 + args.unshift(options) 8 + const argsWithRoot = args.includes('--root') ? args : ['--root', 'fixtures', ...args] 9 + const subprocess = execa('vitest', argsWithRoot, typeof options === 'string' ? undefined : options) 7 10 8 11 let setDone: (value?: unknown) => void 9 12 const isDone = new Promise(resolve => (setDone = resolve)) ··· 23 26 24 27 const timeout = setTimeout(() => { 25 28 reject(new Error(`Timeout when waiting for output "${expected}".\nReceived:\n${this.output}`)) 26 - }, 20_000) 29 + }, process.env.CI ? 20_000 : 4_000) 27 30 28 31 const listener = () => { 29 32 if (this.output.includes(expected)) {
+67
test/watch/test/workspaces.test.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { readFileSync, writeFileSync } from 'node:fs' 3 + import { afterAll, it } from 'vitest' 4 + import { dirname, resolve } from 'pathe' 5 + import { startWatchMode } from './utils' 6 + 7 + const file = fileURLToPath(import.meta.url) 8 + const dir = dirname(file) 9 + const root = resolve(dir, '..', '..', 'workspaces') 10 + const config = resolve(root, 'vitest.config.ts') 11 + 12 + const srcMathFile = resolve(root, 'src', 'math.ts') 13 + const specSpace2File = resolve(root, 'space_2', 'test', 'node.spec.ts') 14 + 15 + const srcMathContent = readFileSync(srcMathFile, 'utf-8') 16 + const specSpace2Content = readFileSync(specSpace2File, 'utf-8') 17 + 18 + function startVitest() { 19 + return startWatchMode( 20 + { cwd: root, env: { TEST_WATCH: 'true' } }, 21 + '--root', 22 + root, 23 + '--config', 24 + config, 25 + '--watch', 26 + '--no-coverage', 27 + ) 28 + } 29 + 30 + afterAll(() => { 31 + writeFileSync(srcMathFile, srcMathContent, 'utf8') 32 + writeFileSync(specSpace2File, specSpace2Content, 'utf8') 33 + }) 34 + 35 + it('editing a test file in a suite with workspaces reruns test', async () => { 36 + const vitest = await startVitest() 37 + 38 + writeFileSync(specSpace2File, `${specSpace2Content}\n`, 'utf8') 39 + 40 + await vitest.waitForOutput('RERUN space_2/test/node.spec.ts x1') 41 + await vitest.waitForOutput('|space_2| test/node.spec.ts') 42 + await vitest.waitForOutput('Test Files 1 passed') 43 + }) 44 + 45 + it('editing a file that is imported in different workspaces reruns both files', async () => { 46 + const vitest = await startVitest() 47 + 48 + writeFileSync(srcMathFile, `${srcMathContent}\n`, 'utf8') 49 + 50 + await vitest.waitForOutput('RERUN src/math.ts') 51 + await vitest.waitForOutput('|space_3| math.space-test.ts') 52 + await vitest.waitForOutput('|space_1| test/math.spec.ts') 53 + await vitest.waitForOutput('Test Files 2 passed') 54 + }) 55 + 56 + it('filters by test name inside a workspace', async () => { 57 + const vitest = await startVitest() 58 + 59 + vitest.write('t') 60 + 61 + await vitest.waitForOutput('Input test name pattern') 62 + 63 + vitest.write('2 x 2 = 4\n') 64 + 65 + await vitest.waitForOutput('Test name pattern: /2 x 2 = 4/') 66 + await vitest.waitForOutput('Test Files 1 passed') 67 + })
+8
test/workspaces/space_1/vite.config.ts
··· 1 + import { defineProject } from 'vitest/config' 2 + 3 + export default defineProject({ 4 + test: { 5 + name: 'space_1', 6 + environment: 'happy-dom', 7 + }, 8 + })
+11
test/workspaces/space_3/math.space-test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { sum } from '../src/math' 3 + import { multiple } from './src/multiply' 4 + 5 + test('2 x 2 = 4', () => { 6 + expect(multiple(2, 2)).toBe(4) 7 + }) 8 + 9 + test('2 + 2 = 4', () => { 10 + expect(sum(2, 2)).toBe(4) 11 + })
+9
test/workspaces/space_3/vitest.config.ts
··· 1 + import { defineProject } from 'vitest/config' 2 + 3 + export default defineProject({ 4 + test: { 5 + include: ['**/*.space-test.ts'], 6 + name: 'space_3', 7 + environment: 'node', 8 + }, 9 + })
+1
test/workspaces/space_shared/setup.jsdom.ts
··· 1 + Object.defineProperty(global, 'testValue', { value: 'jsdom' })
+1
test/workspaces/space_shared/setup.node.ts
··· 1 + Object.defineProperty(global, 'testValue', { value: 'node' })
+9
test/workspaces/space_shared/test.spec.ts
··· 1 + import { expect, it } from 'vitest' 2 + 3 + declare global { 4 + const testValue: string 5 + } 6 + 7 + it('the same file works with different projects', () => { 8 + expect(testValue).toBe(expect.getState().environment === 'node' ? 'node' : 'jsdom') 9 + })
+3
test/workspaces/src/math.ts
··· 1 + export function sum(a: number, b: number) { 2 + return a + b 3 + }
+4
packages/browser/src/client/main.ts
··· 66 66 moduleCache: new Map(), 67 67 rpc: client.rpc, 68 68 safeRpc, 69 + durations: { 70 + environment: 0, 71 + prepare: 0, 72 + }, 69 73 } 70 74 71 75 const paths = getQueryPaths()
+4 -1
packages/vitest/src/api/setup.ts
··· 10 10 import type { Vitest } from '../node' 11 11 import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types' 12 12 import { getModuleGraph, isPrimitive } from '../utils' 13 + import type { WorkspaceProject } from '../node/workspace' 13 14 import { parseErrorStacktrace } from '../utils/source-map' 14 15 import type { TransformResultWithSource, WebSocketEvents, WebSocketHandlers } from './types' 15 16 16 - export function setup(ctx: Vitest, server?: ViteDevServer) { 17 + export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: ViteDevServer) { 18 + const ctx = 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.ctx : vitestOrWorkspace 19 + 17 20 const wss = new WebSocketServer({ noServer: true }) 18 21 19 22 const clients = new Map<WebSocket, BirpcReturn<WebSocketEvents>>()
+2 -2
packages/vitest/src/node/config.ts
··· 4 4 import type { ResolvedConfig as ResolvedViteConfig } from 'vite' 5 5 6 6 import type { ApiConfig, ResolvedConfig, UserConfig, VitestRunMode } from '../types' 7 - import { defaultPort } from '../constants' 7 + import { defaultBrowserPort, defaultPort } from '../constants' 8 8 import { benchmarkConfigDefaults, configDefaults } from '../defaults' 9 9 import { isCI, toArray } from '../utils' 10 10 import { VitestCache } from './cache' ··· 274 274 resolved.browser.headless ??= isCI 275 275 276 276 resolved.browser.api = resolveApiServerConfig(resolved.browser) || { 277 - port: 63315, 277 + port: defaultBrowserPort, 278 278 } 279 279 280 280 return resolved
+266 -248
packages/vitest/src/node/core.ts
··· 1 1 import { existsSync, promises as fs } from 'node:fs' 2 2 import type { ViteDevServer } from 'vite' 3 - import { normalize, relative, toNamespacedPath } from 'pathe' 3 + import { mergeConfig } from 'vite' 4 + import { basename, dirname, join, normalize, relative } from 'pathe' 4 5 import fg from 'fast-glob' 5 6 import mm from 'micromatch' 6 7 import c from 'picocolors' 7 8 import { normalizeRequestId } from 'vite-node/utils' 8 9 import { ViteNodeRunner } from 'vite-node/client' 9 10 import { SnapshotManager } from '@vitest/snapshot/manager' 10 - import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, VitestRunMode } from '../types' 11 - import { deepMerge, hasFailed, noop, slash, toArray } from '../utils' 11 + import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types' 12 + import { hasFailed, noop, slash, toArray } from '../utils' 12 13 import { getCoverageProvider } from '../integrations/coverage' 13 - import { Typechecker } from '../typecheck/typechecker' 14 14 import type { BrowserProvider } from '../types/browser' 15 - import { getBrowserProvider } from '../integrations/browser' 16 - import { createBrowserServer } from '../integrations/browser/server' 15 + import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants' 17 16 import { createPool } from './pool' 18 - import type { ProcessPool } from './pool' 17 + import type { ProcessPool, WorkspaceSpec } from './pool' 19 18 import { createBenchmarkReporters, createReporters } from './reporters/utils' 20 19 import { StateManager } from './state' 21 - import { isBrowserEnabled, resolveConfig } from './config' 20 + import { resolveConfig } from './config' 22 21 import { Logger } from './logger' 23 22 import { VitestCache } from './cache' 23 + import { WorkspaceProject, initializeProject } from './workspace' 24 24 import { VitestServer } from './server' 25 25 26 26 const WATCHER_DEBOUNCE = 100 27 27 28 28 export class Vitest { 29 29 config: ResolvedConfig = undefined! 30 - configOverride: Partial<ResolvedConfig> | undefined 30 + configOverride: Partial<ResolvedConfig> = {} 31 31 32 - browser: ViteDevServer = undefined! 33 32 server: ViteDevServer = undefined! 34 33 state: StateManager = undefined! 35 34 snapshot: SnapshotManager = undefined! ··· 39 38 browserProvider: BrowserProvider | undefined 40 39 logger: Logger 41 40 pool: ProcessPool | undefined 42 - typechecker: Typechecker | undefined 43 41 44 42 vitenode: VitestServer = undefined! 45 43 ··· 53 51 restartsCount = 0 54 52 runner: ViteNodeRunner = undefined! 55 53 54 + private coreWorkspace!: WorkspaceProject 55 + 56 + public projects: WorkspaceProject[] = [] 57 + private projectsTestFiles = new Map<string, Set<WorkspaceProject>>() 58 + 56 59 constructor( 57 60 public readonly mode: VitestRunMode, 58 61 ) { ··· 62 65 private _onRestartListeners: OnServerRestartHandler[] = [] 63 66 private _onSetServer: OnServerRestartHandler[] = [] 64 67 65 - async initBrowserServer(options: UserConfig) { 66 - if (!this.isBrowserEnabled()) 67 - return 68 - await this.browser?.close() 69 - this.browser = await createBrowserServer(this, options) 70 - } 71 - 72 - async setServer(options: UserConfig, server: ViteDevServer) { 68 + async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { 73 69 this.unregisterWatcher?.() 74 70 clearTimeout(this._rerunTimer) 75 71 this.restartsCount += 1 ··· 129 125 try { 130 126 await this.cache.results.readFromCache() 131 127 } 132 - catch {} 128 + catch { } 133 129 134 130 await Promise.all(this._onSetServer.map(fn => fn())) 131 + 132 + this.projects = await this.resolveWorkspace(options, cliOptions) 133 + 134 + if (this.config.testNamePattern) 135 + this.configOverride.testNamePattern = this.config.testNamePattern 135 136 } 136 137 137 - async initCoverageProvider() { 138 + private async createCoreWorkspace(options: UserConfig) { 139 + const coreWorkspace = new WorkspaceProject(this.config.root, this) 140 + await coreWorkspace.setServer(options, this.server, { 141 + runner: this.runner, 142 + server: this.vitenode, 143 + }) 144 + this.coreWorkspace = coreWorkspace 145 + return coreWorkspace 146 + } 147 + 148 + public getCoreWorkspaceProject() { 149 + if (!this.coreWorkspace) 150 + throw new Error('Core workspace project is not initialized') 151 + return this.coreWorkspace 152 + } 153 + 154 + private async resolveWorkspace(options: UserConfig, cliOptions: UserConfig) { 155 + const configDir = dirname(this.server.config.configFile || this.config.root) 156 + const rootFiles = await fs.readdir(configDir) 157 + const workspaceConfigName = workspaceFiles.find((configFile) => { 158 + return rootFiles.includes(configFile) 159 + }) 160 + 161 + if (!workspaceConfigName) 162 + return [await this.createCoreWorkspace(options)] 163 + 164 + const workspacesConfigPath = join(configDir, workspaceConfigName) 165 + 166 + const workspacesModule = await this.runner.executeFile(workspacesConfigPath) as { 167 + default: (string | UserWorkspaceConfig)[] 168 + } 169 + 170 + if (!workspacesModule.default || !Array.isArray(workspacesModule.default)) 171 + throw new Error(`Workspace config file ${workspacesConfigPath} must export a default array of project paths.`) 172 + 173 + const workspacesGlobMatches: string[] = [] 174 + const projectsOptions: UserWorkspaceConfig[] = [] 175 + 176 + for (const project of workspacesModule.default) { 177 + if (typeof project === 'string') 178 + workspacesGlobMatches.push(project.replace('<rootDir>', this.config.root)) 179 + else 180 + projectsOptions.push(project) 181 + } 182 + 183 + const globOptions: fg.Options = { 184 + absolute: true, 185 + dot: true, 186 + onlyFiles: false, 187 + markDirectories: true, 188 + cwd: this.config.root, 189 + ignore: ['**/node_modules/**'], 190 + } 191 + 192 + const workspacesFs = await fg(workspacesGlobMatches, globOptions) 193 + const resolvedWorkspacesPaths = await Promise.all(workspacesFs.filter((file) => { 194 + if (file.endsWith('/')) { 195 + // if it's a directory, check that we don't already have a workspace with a config inside 196 + const hasWorkspaceWithConfig = workspacesFs.some((file2) => { 197 + return file2 !== file && `${dirname(file2)}/` === file 198 + }) 199 + return !hasWorkspaceWithConfig 200 + } 201 + const filename = basename(file) 202 + return CONFIG_NAMES.some(configName => filename.startsWith(configName)) 203 + }).map(async (filepath) => { 204 + if (filepath.endsWith('/')) { 205 + const filesInside = await fs.readdir(filepath) 206 + const configFile = configFiles.find(config => filesInside.includes(config)) 207 + return configFile || filepath 208 + } 209 + return filepath 210 + })) 211 + 212 + const overridesOptions = [ 213 + 'logHeapUsage', 214 + 'allowOnly', 215 + 'sequence', 216 + 'testTimeout', 217 + 'threads', 218 + 'singleThread', 219 + 'isolate', 220 + 'globals', 221 + 'mode', 222 + ] as const 223 + 224 + const cliOverrides = overridesOptions.reduce((acc, name) => { 225 + if (name in cliOptions) 226 + acc[name] = cliOptions[name] as any 227 + return acc 228 + }, {} as UserConfig) 229 + 230 + const projects = resolvedWorkspacesPaths.map(async (workspacePath) => { 231 + // don't start a new server, but reuse existing one 232 + if ( 233 + this.server.config.configFile === workspacePath 234 + ) 235 + return this.createCoreWorkspace(options) 236 + return initializeProject(workspacePath, this, { test: cliOverrides }) 237 + }) 238 + 239 + projectsOptions.forEach((options, index) => { 240 + projects.push(initializeProject(index, this, mergeConfig(options, { test: cliOverrides }))) 241 + }) 242 + 243 + if (!projects.length) 244 + return [await this.createCoreWorkspace(options)] 245 + 246 + const resolvedProjects = await Promise.all(projects) 247 + const names = new Set<string>() 248 + 249 + for (const project of resolvedProjects) { 250 + const name = project.getName() 251 + if (names.has(name)) 252 + throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`) 253 + names.add(name) 254 + } 255 + 256 + return resolvedProjects 257 + } 258 + 259 + private async initCoverageProvider() { 138 260 if (this.coverageProvider !== undefined) 139 261 return 140 262 this.coverageProvider = await getCoverageProvider(this.config.coverage, this.runner) ··· 145 267 return this.coverageProvider 146 268 } 147 269 148 - async initBrowserProvider() { 149 - if (this.browserProvider) 150 - return this.browserProvider 151 - const Provider = await getBrowserProvider(this.config.browser, this.runner) 152 - this.browserProvider = new Provider() 153 - const browser = this.config.browser.name 154 - const supportedBrowsers = this.browserProvider.getSupportedBrowsers() 155 - if (!browser) 156 - throw new Error('Browser name is required. Please, set `test.browser.name` option manually.') 157 - if (!supportedBrowsers.includes(browser)) 158 - throw new Error(`Browser "${browser}" is not supported by the browser provider "${this.browserProvider.name}". Supported browsers: ${supportedBrowsers.join(', ')}.`) 159 - await this.browserProvider.initialize(this, { browser }) 160 - return this.browserProvider 270 + private async initBrowserProviders() { 271 + return Promise.all(this.projects.map(w => w.initBrowserProvider())) 161 272 } 162 273 163 - getSerializableConfig() { 164 - return deepMerge({ 165 - ...this.config, 166 - reporters: [], 167 - deps: { 168 - ...this.config.deps, 169 - experimentalOptimizer: { 170 - enabled: this.config.deps?.experimentalOptimizer?.enabled ?? false, 171 - }, 172 - }, 173 - snapshotOptions: { 174 - ...this.config.snapshotOptions, 175 - resolveSnapshotPath: undefined, 176 - }, 177 - onConsoleLog: undefined!, 178 - sequence: { 179 - ...this.config.sequence, 180 - sequencer: undefined!, 181 - }, 182 - benchmark: { 183 - ...this.config.benchmark, 184 - reporters: [], 185 - }, 186 - }, 187 - this.configOverride || {} as any, 188 - ) as ResolvedConfig 189 - } 190 - 191 - async typecheck(filters: string[] = []) { 192 - const { dir, root } = this.config 193 - const { include, exclude } = this.config.typecheck 194 - const testsFilesList = this.filterFiles(await this.globFiles(include, exclude, dir || root), filters) 195 - const checker = new Typechecker(this, testsFilesList) 196 - this.typechecker = checker 197 - checker.onParseEnd(async ({ files, sourceErrors }) => { 198 - this.state.collectFiles(checker.getTestFiles()) 199 - await this.report('onTaskUpdate', checker.getTestPacks()) 200 - await this.report('onCollected') 201 - if (!files.length) { 202 - this.logger.printNoTestFound() 203 - } 204 - else { 205 - if (hasFailed(files)) 206 - process.exitCode = 1 207 - await this.report('onFinished', files) 208 - } 209 - if (sourceErrors.length && !this.config.typecheck.ignoreSourceErrors) { 210 - process.exitCode = 1 211 - await this.logger.printSourceTypeErrors(sourceErrors) 212 - } 213 - // if there are source errors, we are showing it, and then terminating process 214 - if (!files.length) { 215 - const exitCode = this.config.passWithNoTests ? (process.exitCode ?? 0) : 1 216 - process.exit(exitCode) 217 - } 218 - if (this.config.watch) { 219 - await this.report('onWatcherStart', files, [ 220 - ...(this.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), 221 - ...this.state.getUnhandledErrors(), 222 - ]) 223 - } 224 - }) 225 - checker.onParseStart(async () => { 226 - await this.report('onInit', this) 227 - this.state.collectFiles(checker.getTestFiles()) 228 - await this.report('onCollected') 229 - }) 230 - checker.onWatcherRerun(async () => { 231 - await this.report('onWatcherRerun', testsFilesList, 'File change detected. Triggering rerun.') 232 - await checker.collectTests() 233 - this.state.collectFiles(checker.getTestFiles()) 234 - await this.report('onTaskUpdate', checker.getTestPacks()) 235 - await this.report('onCollected') 236 - }) 237 - await checker.prepare() 238 - await checker.collectTests() 239 - await checker.start() 274 + typecheck(filters?: string[]) { 275 + return Promise.all(this.projects.map(project => project.typecheck(filters))) 240 276 } 241 277 242 278 async start(filters?: string[]) { ··· 248 284 try { 249 285 await this.initCoverageProvider() 250 286 await this.coverageProvider?.clean(this.config.coverage.clean) 251 - 252 - if (this.isBrowserEnabled()) 253 - await this.initBrowserProvider() 287 + await this.initBrowserProviders() 254 288 } 255 289 catch (e) { 256 290 this.logger.error(e) ··· 273 307 } 274 308 275 309 // populate once, update cache on watch 276 - await Promise.all(files.map(file => this.cache.stats.updateStats(file))) 310 + await this.cache.stats.populateStats(this.config.root, files) 277 311 278 312 await this.runFiles(files) 279 313 ··· 283 317 await this.report('onWatcherStart') 284 318 } 285 319 286 - private async getTestDependencies(filepath: string) { 320 + private async getTestDependencies(filepath: WorkspaceSpec) { 287 321 const deps = new Set<string>() 288 322 289 - const addImports = async (filepath: string) => { 290 - const transformed = await this.vitenode.transformRequest(filepath) 323 + const addImports = async ([project, filepath]: WorkspaceSpec) => { 324 + const transformed = await project.vitenode.transformRequest(filepath) 291 325 if (!transformed) 292 326 return 293 327 const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] ··· 297 331 if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { 298 332 deps.add(fsPath) 299 333 300 - await addImports(fsPath) 334 + await addImports([project, fsPath]) 301 335 } 302 336 } 303 337 } ··· 307 341 return deps 308 342 } 309 343 310 - async filterTestsBySource(tests: string[]) { 344 + async filterTestsBySource(specs: WorkspaceSpec[]) { 311 345 if (this.config.changed && !this.config.related) { 312 346 const { VitestGit } = await import('./git') 313 347 const vitestGit = new VitestGit(this.config.root) ··· 323 357 324 358 const related = this.config.related 325 359 if (!related) 326 - return tests 360 + return specs 327 361 328 362 const forceRerunTriggers = this.config.forceRerunTriggers 329 363 if (forceRerunTriggers.length && mm(related, forceRerunTriggers).length) 330 - return tests 364 + return specs 331 365 332 366 // don't run anything if no related sources are found 333 367 if (!related.length) 334 368 return [] 335 369 336 370 const testGraphs = await Promise.all( 337 - tests.map(async (filepath) => { 338 - const deps = await this.getTestDependencies(filepath) 339 - return [filepath, deps] as const 371 + specs.map(async (spec) => { 372 + const deps = await this.getTestDependencies(spec) 373 + return [spec, deps] as const 340 374 }), 341 375 ) 342 376 ··· 344 378 345 379 for (const [filepath, deps] of testGraphs) { 346 380 // if deps or the test itself were changed 347 - if (related.some(path => path === filepath || deps.has(path))) 381 + if (related.some(path => path === filepath[1] || deps.has(path))) 348 382 runningTests.push(filepath) 349 383 } 350 384 351 385 return runningTests 352 386 } 353 387 354 - async runFiles(paths: string[]) { 355 - paths = Array.from(new Set(paths)) 388 + getProjectsByTestFile(file: string) { 389 + const projects = this.projectsTestFiles.get(file) 390 + if (!projects) 391 + return [] 392 + return Array.from(projects).map(project => [project, file] as WorkspaceSpec) 393 + } 356 394 357 - this.state.collectPaths(paths) 395 + async runFiles(paths: WorkspaceSpec[]) { 396 + const filepaths = paths.map(([, file]) => file) 358 397 359 - await this.report('onPathsCollected', paths) 398 + this.state.collectPaths(filepaths) 399 + 400 + await this.report('onPathsCollected', filepaths) 360 401 361 402 // previous run 362 403 await this.runningPromise ··· 386 427 await this.cache.results.writeToCache() 387 428 })() 388 429 .finally(async () => { 389 - await this.report('onFinished', this.state.getFiles(paths), this.state.getUnhandledErrors()) 430 + // can be duplicate files if different projects are using the same file 431 + const specs = Array.from(new Set(paths.map(([, p]) => p))) 432 + await this.report('onFinished', this.state.getFiles(specs), this.state.getUnhandledErrors()) 390 433 this.runningPromise = undefined 391 434 }) 392 435 ··· 396 439 async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string) { 397 440 if (this.filenamePattern) { 398 441 const filteredFiles = await this.globTestFiles([this.filenamePattern]) 399 - files = files.filter(file => filteredFiles.includes(file)) 442 + files = files.filter(file => filteredFiles.some(f => f[1] === file)) 400 443 } 401 444 402 445 if (this.coverageProvider && this.config.coverage.cleanOnRerun) 403 446 await this.coverageProvider.clean() 404 447 405 448 await this.report('onWatcherRerun', files, trigger) 406 - await this.runFiles(files) 449 + await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file))) 407 450 408 451 await this.reportCoverage(!trigger) 409 452 ··· 415 458 if (pattern === '') 416 459 this.filenamePattern = undefined 417 460 418 - this.config.testNamePattern = pattern ? new RegExp(pattern) : undefined 461 + this.configOverride.testNamePattern = pattern ? new RegExp(pattern) : undefined 419 462 await this.rerunFiles(files, trigger) 420 463 } 421 464 ··· 439 482 ...this.snapshot.summary.uncheckedKeysByFile.map(s => s.filePath), 440 483 ] 441 484 442 - this.configOverride = { 443 - snapshotOptions: { 444 - updateSnapshot: 'all', 445 - // environment is resolved inside a worker thread 446 - snapshotEnvironment: null as any, 447 - }, 485 + this.configOverride.snapshotOptions = { 486 + updateSnapshot: 'all', 487 + // environment is resolved inside a worker thread 488 + snapshotEnvironment: null as any, 448 489 } 449 490 450 491 try { 451 492 await this.rerunFiles(files, 'update snapshot') 452 493 } 453 494 finally { 454 - this.configOverride = undefined 495 + delete this.configOverride.snapshotOptions 455 496 } 456 497 } 457 498 458 499 private _rerunTimer: any 459 - private async scheduleRerun(triggerId: string) { 500 + private async scheduleRerun(triggerId: string[]) { 460 501 const currentCount = this.restartsCount 461 502 clearTimeout(this._rerunTimer) 462 503 await this.runningPromise ··· 483 524 484 525 if (this.filenamePattern) { 485 526 const filteredFiles = await this.globTestFiles([this.filenamePattern]) 486 - files = files.filter(file => filteredFiles.includes(file)) 527 + files = files.filter(file => filteredFiles.some(f => f[1] === file)) 487 528 488 529 // A file that does not match the current filename pattern was changed 489 530 if (files.length === 0) ··· 495 536 if (this.coverageProvider && this.config.coverage.cleanOnRerun) 496 537 await this.coverageProvider.clean() 497 538 498 - await this.report('onWatcherRerun', files, triggerId) 539 + const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) 540 + const triggerLabel = Array.from(triggerIds).join(', ') 541 + await this.report('onWatcherRerun', files, triggerLabel) 499 542 500 - await this.runFiles(files) 543 + await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file))) 501 544 502 545 await this.reportCoverage(false) 503 546 ··· 505 548 }, WATCHER_DEBOUNCE) 506 549 } 507 550 551 + public getModuleProjects(id: string) { 552 + return this.projects.filter((project) => { 553 + return project.server.moduleGraph.getModuleById(id) 554 + || project.browser?.moduleGraph.getModuleById(id) 555 + || project.browser?.moduleGraph.getModulesByFile(id)?.size 556 + }) 557 + } 558 + 508 559 private unregisterWatcher = noop 509 560 private registerWatcher() { 510 561 const updateLastChanged = (id: string) => { 511 - const mod = this.server.moduleGraph.getModuleById(id) || this.browser?.moduleGraph.getModuleById(id) 512 - if (mod) 513 - mod.lastHMRTimestamp = Date.now() 562 + const projects = this.getModuleProjects(id) 563 + projects.forEach(({ server, browser }) => { 564 + const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id) 565 + if (mod) 566 + server.moduleGraph.invalidateModule(mod) 567 + }) 514 568 } 515 569 516 570 const onChange = (id: string) => { 517 571 id = slash(id) 518 572 updateLastChanged(id) 519 573 const needsRerun = this.handleFileChanged(id) 520 - if (needsRerun) 521 - this.scheduleRerun(id) 574 + if (needsRerun.length) 575 + this.scheduleRerun(needsRerun) 522 576 } 523 577 const onUnlink = (id: string) => { 524 578 id = slash(id) ··· 537 591 updateLastChanged(id) 538 592 if (await this.isTargetFile(id)) { 539 593 this.changedTests.add(id) 540 - await this.cache.stats.updateStats(id) 541 - this.scheduleRerun(id) 594 + this.scheduleRerun([id]) 542 595 } 543 596 } 544 597 const watcher = this.server.watcher ··· 563 616 /** 564 617 * @returns A value indicating whether rerun is needed (changedTests was mutated) 565 618 */ 566 - private handleFileChanged(id: string): boolean { 619 + private handleFileChanged(id: string): string[] { 567 620 if (this.changedTests.has(id) || this.invalidates.has(id)) 568 - return false 621 + return [] 569 622 570 623 if (mm.isMatch(id, this.config.forceRerunTriggers)) { 571 624 this.state.getFilepaths().forEach(file => this.changedTests.add(file)) 572 - return true 625 + return [] 573 626 } 574 627 575 - const mod = this.server.moduleGraph.getModuleById(id) || this.browser?.moduleGraph.getModuleById(id) 576 - if (!mod) { 577 - // files with `?v=` query from the browser 578 - const mods = this.browser?.moduleGraph.getModulesByFile(id) 579 - if (!mods?.size) 580 - return false 628 + const projects = this.getModuleProjects(id) 629 + if (!projects.length) 630 + return [] 631 + 632 + const files: string[] = [] 633 + 634 + for (const { server, browser } of projects) { 635 + const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id) 636 + if (!mod) { 637 + // files with `?v=` query from the browser 638 + const mods = browser?.moduleGraph.getModulesByFile(id) 639 + if (!mods?.size) 640 + return [] 641 + let rerun = false 642 + mods.forEach((m) => { 643 + if (m.id && this.handleFileChanged(m.id)) 644 + rerun = true 645 + }) 646 + if (rerun) 647 + files.push(id) 648 + continue 649 + } 650 + 651 + // remove queries from id 652 + id = normalizeRequestId(id, server.config.base) 653 + 654 + this.invalidates.add(id) 655 + 656 + if (this.state.filesMap.has(id)) { 657 + this.changedTests.add(id) 658 + files.push(id) 659 + continue 660 + } 661 + 581 662 let rerun = false 582 - mods.forEach((m) => { 583 - if (m.id && this.handleFileChanged(m.id)) 663 + mod.importers.forEach((i) => { 664 + if (!i.id) 665 + return 666 + 667 + const heedsRerun = this.handleFileChanged(i.id) 668 + if (heedsRerun) 584 669 rerun = true 585 670 }) 586 - return rerun 671 + 672 + if (rerun) 673 + files.push(id) 587 674 } 588 675 589 - // remove queries from id 590 - id = normalizeRequestId(id, this.server.config.base) 591 - 592 - this.invalidates.add(id) 593 - 594 - if (this.state.filesMap.has(id)) { 595 - this.changedTests.add(id) 596 - return true 597 - } 598 - 599 - let rerun = false 600 - mod.importers.forEach((i) => { 601 - if (!i.id) 602 - return 603 - 604 - const heedsRerun = this.handleFileChanged(i.id) 605 - if (heedsRerun) 606 - rerun = true 607 - }) 608 - 609 - return rerun 676 + return files 610 677 } 611 678 612 679 private async reportCoverage(allTestsRun: boolean) { ··· 620 687 if (!this.closingPromise) { 621 688 this.closingPromise = Promise.allSettled([ 622 689 this.pool?.close(), 623 - this.browser?.close(), 624 690 this.server.close(), 625 - this.typechecker?.stop(), 691 + ...this.projects.map(w => w.close()), 626 692 ].filter(Boolean)).then((results) => { 627 693 results.filter(r => r.status === 'rejected').forEach((err) => { 628 694 this.logger.error('error during close', (err as PromiseRejectedResult).reason) ··· 656 722 ))) 657 723 } 658 724 659 - async globFiles(include: string[], exclude: string[], cwd: string) { 660 - const globOptions: fg.Options = { 661 - absolute: true, 662 - dot: true, 663 - cwd, 664 - ignore: exclude, 665 - } 666 - 667 - return fg(include, globOptions) 725 + public async globTestFiles(filters: string[] = []) { 726 + const files: WorkspaceSpec[] = [] 727 + await Promise.all(this.projects.map(async (project) => { 728 + const specs = await project.globTestFiles(filters) 729 + specs.forEach((file) => { 730 + files.push([project, file]) 731 + const projects = this.projectsTestFiles.get(file) || new Set() 732 + projects.add(project) 733 + this.projectsTestFiles.set(file, projects) 734 + }) 735 + })) 736 + return files 668 737 } 669 738 670 - private _allTestsCache: string[] | null = null 671 - 672 - async globAllTestFiles(config: ResolvedConfig, cwd: string) { 673 - const { include, exclude, includeSource } = config 674 - 675 - const testFiles = await this.globFiles(include, exclude, cwd) 676 - 677 - if (includeSource) { 678 - const files = await this.globFiles(includeSource, exclude, cwd) 679 - 680 - await Promise.all(files.map(async (file) => { 681 - try { 682 - const code = await fs.readFile(file, 'utf-8') 683 - if (this.isInSourceTestFile(code)) 684 - testFiles.push(file) 685 - } 686 - catch { 687 - return null 688 - } 689 - })) 690 - } 691 - 692 - this._allTestsCache = testFiles 693 - 694 - return testFiles 695 - } 696 - 697 - filterFiles(testFiles: string[], filters: string[] = []) { 698 - if (filters.length && process.platform === 'win32') 699 - filters = filters.map(f => toNamespacedPath(f)) 700 - 701 - if (filters.length) 702 - return testFiles.filter(i => filters.some(f => i.includes(f))) 703 - 704 - return testFiles 705 - } 706 - 707 - async globTestFiles(filters: string[] = []) { 708 - const { dir, root } = this.config 709 - 710 - const testFiles = this._allTestsCache ?? await this.globAllTestFiles(this.config, dir || root) 711 - 712 - this._allTestsCache = null 713 - 714 - return this.filterFiles(testFiles, filters) 715 - } 716 - 717 - async isTargetFile(id: string, source?: string): Promise<boolean> { 739 + private async isTargetFile(id: string, source?: string): Promise<boolean> { 718 740 const relativeId = relative(this.config.dir || this.config.root, id) 719 741 if (mm.isMatch(relativeId, this.config.exclude)) 720 742 return false ··· 725 747 return this.isInSourceTestFile(source) 726 748 } 727 749 return false 728 - } 729 - 730 - isBrowserEnabled() { 731 - return isBrowserEnabled(this.config) 732 750 } 733 751 734 752 // The server needs to be running for communication
+1 -1
packages/vitest/src/node/error.ts
··· 47 47 const nearest = error instanceof TypeCheckError 48 48 ? error.stacks[0] 49 49 : stacks.find(stack => 50 - ctx.server.moduleGraph.getModuleById(stack.file) 50 + ctx.getModuleProjects(stack.file).length 51 51 && existsSync(stack.file), 52 52 ) 53 53
+2
packages/vitest/src/node/index.ts
··· 1 1 export type { Vitest } from './core' 2 + export type { WorkspaceProject as VitestWorkspace } from './workspace' 2 3 export { createVitest } from './create' 3 4 export { VitestPlugin } from './plugins' 4 5 export { startVitest } from './cli-api' 6 + export type { WorkspaceSpec } from './pool' 5 7 6 8 export { VitestExecutor } from '../runtime/execute' 7 9 export type { ExecuteOptions } from '../runtime/execute'
+9 -2
packages/vitest/src/node/logger.ts
··· 109 109 if (this.ctx.config.sequence.sequencer === RandomSequencer) 110 110 this.log(c.gray(` Running tests with seed "${this.ctx.config.sequence.seed}"`)) 111 111 112 - if (this.ctx.config.browser.enabled) 113 - this.log(c.dim(c.green(` Browser runner started at http://${this.ctx.config.browser.api?.host || 'localhost'}:${c.bold(`${this.ctx.browser.config.server.port}`)}`))) 112 + this.ctx.projects.forEach((project) => { 113 + if (!project.browser) 114 + return 115 + const name = project.getName() 116 + const output = project.isCore() ? '' : ` [${name}]` 117 + 118 + this.log(c.dim(c.green(` ${output} Browser runner started at http://${project.config.browser.api?.host || 'localhost'}:${c.bold(`${project.browser.config.server.port}`)}`))) 119 + }) 120 + 114 121 if (this.ctx.config.ui) 115 122 this.log(c.dim(c.green(` UI started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.server.config.server.port}`)}${this.ctx.config.uiBase}`))) 116 123 else if (this.ctx.config.api)
+18 -22
packages/vitest/src/node/pool.ts
··· 7 7 import { createChildProcessPool } from './pools/child' 8 8 import { createThreadsPool } from './pools/threads' 9 9 import { createBrowserPool } from './pools/browser' 10 + import type { WorkspaceProject } from './workspace' 10 11 11 - export type RunWithFiles = (files: string[], invalidates?: string[]) => Promise<void> 12 + export type WorkspaceSpec = [project: WorkspaceProject, testFile: string] 13 + export type RunWithFiles = (files: WorkspaceSpec[], invalidates?: string[]) => Promise<void> 12 14 13 15 export interface ProcessPool { 14 16 runTests: RunWithFiles ··· 30 32 browser: null, 31 33 } 32 34 33 - function getDefaultPoolName() { 34 - if (ctx.config.browser.enabled) 35 + function getDefaultPoolName(project: WorkspaceProject) { 36 + if (project.config.browser.enabled) 35 37 return 'browser' 36 - if (ctx.config.threads) 38 + if (project.config.threads) 37 39 return 'threads' 38 40 return 'child_process' 39 41 } 40 42 41 - function getPoolName(file: string) { 42 - for (const [glob, pool] of ctx.config.poolMatchGlobs || []) { 43 - if (mm.isMatch(file, glob, { cwd: ctx.server.config.root })) 43 + function getPoolName([project, file]: WorkspaceSpec) { 44 + for (const [glob, pool] of project.config.poolMatchGlobs || []) { 45 + if (mm.isMatch(file, glob, { cwd: project.config.root })) 44 46 return pool 45 47 } 46 - return getDefaultPoolName() 48 + return getDefaultPoolName(project) 47 49 } 48 50 49 - async function runTests(files: string[], invalidate?: string[]) { 51 + async function runTests(files: WorkspaceSpec[], invalidate?: string[]) { 50 52 const conditions = ctx.server.config.resolve.conditions?.flatMap(c => ['--conditions', c]) || [] 51 53 52 54 // Instead of passing whole process.execArgv to the workers, pick allowed options. ··· 79 81 } 80 82 81 83 const filesByPool = { 82 - child_process: [] as string[], 83 - threads: [] as string[], 84 - browser: [] as string[], 84 + child_process: [] as WorkspaceSpec[], 85 + threads: [] as WorkspaceSpec[], 86 + browser: [] as WorkspaceSpec[], 85 87 } 86 88 87 - if (!ctx.config.poolMatchGlobs) { 88 - const name = getDefaultPoolName() 89 - filesByPool[name] = files 90 - } 91 - else { 92 - for (const file of files) { 93 - const pool = getPoolName(file) 94 - filesByPool[pool].push(file) 95 - } 89 + for (const spec of files) { 90 + const pool = getPoolName(spec) 91 + filesByPool[pool].push(spec) 96 92 } 97 93 98 94 await Promise.all(Object.entries(filesByPool).map(([pool, files]) => { 99 95 if (!files.length) 100 96 return null 101 97 102 - if (ctx.browserProvider && pool === 'browser') { 98 + if (pool === 'browser') { 103 99 pools.browser ??= createBrowserPool(ctx) 104 100 return pools.browser.runTests(files, invalidate) 105 101 }
+17 -6
packages/vitest/src/node/state.ts
··· 1 1 import type { ErrorWithDiff, File, Task, TaskResultPack, UserConsoleLog } from '../types' 2 2 // can't import actual functions from utils, because it's incompatible with @vitest/browsers 3 3 import type { AggregateError as AggregateErrorPonyfill } from '../utils' 4 + import type { WorkspaceProject } from './workspace' 4 5 5 6 interface CollectingPromise { 6 7 promise: Promise<void> ··· 16 17 17 18 // Note this file is shared for both node and browser, be aware to avoid node specific logic 18 19 export class StateManager { 19 - filesMap = new Map<string, File>() 20 + filesMap = new Map<string, File[]>() 20 21 pathsSet: Set<string> = new Set() 21 22 collectingPromise: CollectingPromise | undefined = undefined 22 23 browserTestPromises = new Map<string, { resolve: (v: unknown) => void; reject: (v: unknown) => void }>() ··· 55 56 56 57 getFiles(keys?: string[]): File[] { 57 58 if (keys) 58 - return keys.map(key => this.filesMap.get(key)!).filter(Boolean) 59 - return Array.from(this.filesMap.values()) 59 + return keys.map(key => this.filesMap.get(key)!).filter(Boolean).flat() 60 + return Array.from(this.filesMap.values()).flat() 60 61 } 61 62 62 63 getFilepaths(): string[] { ··· 77 78 78 79 collectFiles(files: File[] = []) { 79 80 files.forEach((file) => { 80 - this.filesMap.set(file.filepath, file) 81 + const existing = (this.filesMap.get(file.filepath) || []) 82 + const otherProject = existing.filter(i => i.projectName !== file.projectName) 83 + otherProject.push(file) 84 + this.filesMap.set(file.filepath, otherProject) 81 85 this.updateId(file) 82 86 }) 83 87 } 84 88 85 - clearFiles(paths: string[] = []) { 89 + clearFiles(project: WorkspaceProject, paths: string[] = []) { 86 90 paths.forEach((path) => { 87 - this.filesMap.delete(path) 91 + const files = this.filesMap.get(path) 92 + if (!files) 93 + return 94 + const filtered = files.filter(file => file.projectName !== project.config.name) 95 + if (!filtered.length) 96 + this.filesMap.delete(path) 97 + else 98 + this.filesMap.set(path, filtered) 88 99 }) 89 100 } 90 101
+1 -1
packages/vitest/src/node/stdin.ts
··· 81 81 name: 'filter', 82 82 type: 'text', 83 83 message: 'Input test name pattern (RegExp)', 84 - initial: ctx.config.testNamePattern?.source || '', 84 + initial: ctx.configOverride.testNamePattern?.source || '', 85 85 }]) 86 86 await ctx.changeNamePattern(filter.trim(), undefined, 'change pattern') 87 87 on()
+281
packages/vitest/src/node/workspace.ts
··· 1 + import { promises as fs } from 'node:fs' 2 + import fg from 'fast-glob' 3 + import { dirname, resolve, toNamespacedPath } from 'pathe' 4 + import { createServer } from 'vite' 5 + import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' 6 + import { ViteNodeRunner } from 'vite-node/client' 7 + import { createBrowserServer } from '../integrations/browser/server' 8 + import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' 9 + import { deepMerge, hasFailed } from '../utils' 10 + import { Typechecker } from '../typecheck/typechecker' 11 + import type { BrowserProvider } from '../types/browser' 12 + import { getBrowserProvider } from '../integrations/browser' 13 + import { isBrowserEnabled, resolveConfig } from './config' 14 + import { WorkspaceVitestPlugin } from './plugins/workspace' 15 + import { VitestServer } from './server' 16 + 17 + interface InitializeOptions { 18 + server?: VitestServer 19 + runner?: ViteNodeRunner 20 + } 21 + 22 + export async function initializeProject(workspacePath: string | number, ctx: Vitest, options: UserWorkspaceConfig = {}) { 23 + const project = new WorkspaceProject(workspacePath, ctx) 24 + 25 + const configFile = options.extends 26 + ? resolve(ctx.config.root, options.extends) 27 + : (typeof workspacePath === 'number' || workspacePath.endsWith('/')) 28 + ? false 29 + : workspacePath 30 + 31 + const root = options.root || (typeof workspacePath === 'number' ? undefined : dirname(workspacePath)) 32 + 33 + const config: ViteInlineConfig = { 34 + ...options, 35 + root, 36 + logLevel: 'error', 37 + configFile, 38 + // this will make "mode" = "test" inside defineConfig 39 + mode: options.mode || ctx.config.mode || process.env.NODE_ENV, 40 + plugins: [ 41 + ...options.plugins || [], 42 + WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), 43 + ], 44 + } 45 + 46 + const server = await createServer(config) 47 + 48 + // optimizer needs .listen() to be called 49 + if (ctx.config.api?.port || project.config.deps?.experimentalOptimizer?.enabled) 50 + await server.listen() 51 + else 52 + await server.pluginContainer.buildStart({}) 53 + 54 + return project 55 + } 56 + 57 + export class WorkspaceProject { 58 + configOverride: Partial<ResolvedConfig> | undefined 59 + 60 + config!: ResolvedConfig 61 + server!: ViteDevServer 62 + vitenode!: VitestServer 63 + runner!: ViteNodeRunner 64 + browser: ViteDevServer = undefined! 65 + typechecker?: Typechecker 66 + 67 + closingPromise: Promise<unknown> | undefined 68 + browserProvider: BrowserProvider | undefined 69 + 70 + constructor( 71 + public path: string | number, 72 + public ctx: Vitest, 73 + ) { } 74 + 75 + getName(): string { 76 + return this.config.name || '' 77 + } 78 + 79 + isCore() { 80 + return this.ctx.getCoreWorkspaceProject() === this 81 + } 82 + 83 + get reporters() { 84 + return this.ctx.reporters 85 + } 86 + 87 + async globTestFiles(filters: string[] = []) { 88 + const { dir, root } = this.config 89 + 90 + const testFiles = await this.globAllTestFiles(this.config, dir || root) 91 + 92 + return this.filterFiles(testFiles, filters) 93 + } 94 + 95 + async globAllTestFiles(config: ResolvedConfig, cwd: string) { 96 + const { include, exclude, includeSource } = config 97 + 98 + const testFiles = await this.globFiles(include, exclude, cwd) 99 + 100 + if (includeSource) { 101 + const files = await this.globFiles(includeSource, exclude, cwd) 102 + 103 + await Promise.all(files.map(async (file) => { 104 + try { 105 + const code = await fs.readFile(file, 'utf-8') 106 + if (this.ctx.isInSourceTestFile(code)) 107 + testFiles.push(file) 108 + } 109 + catch { 110 + return null 111 + } 112 + })) 113 + } 114 + 115 + return testFiles 116 + } 117 + 118 + async globFiles(include: string[], exclude: string[], cwd: string) { 119 + const globOptions: fg.Options = { 120 + absolute: true, 121 + dot: true, 122 + cwd, 123 + ignore: exclude, 124 + } 125 + 126 + return fg(include, globOptions) 127 + } 128 + 129 + filterFiles(testFiles: string[], filters: string[] = []) { 130 + if (filters.length && process.platform === 'win32') 131 + filters = filters.map(f => toNamespacedPath(f)) 132 + 133 + if (filters.length) 134 + return testFiles.filter(i => filters.some(f => i.includes(f))) 135 + 136 + return testFiles 137 + } 138 + 139 + async initBrowserServer(options: UserConfig) { 140 + if (!this.isBrowserEnabled()) 141 + return 142 + await this.browser?.close() 143 + this.browser = await createBrowserServer(this, options) 144 + } 145 + 146 + async setServer(options: UserConfig, server: ViteDevServer, params: InitializeOptions = {}) { 147 + this.config = resolveConfig(this.ctx.mode, options, server.config) 148 + this.server = server 149 + 150 + this.vitenode = params.server ?? new VitestServer(server, this.config) 151 + const node = this.vitenode 152 + this.runner = params.runner ?? new ViteNodeRunner({ 153 + root: server.config.root, 154 + base: server.config.base, 155 + fetchModule(id: string) { 156 + return node.fetchModule(id) 157 + }, 158 + resolveId(id: string, importer?: string) { 159 + return node.resolveId(id, importer) 160 + }, 161 + }) 162 + 163 + await this.initBrowserServer(options) 164 + } 165 + 166 + async report<T extends keyof Reporter>(name: T, ...args: ArgumentsType<Reporter[T]>) { 167 + return this.ctx.report(name, ...args) 168 + } 169 + 170 + async typecheck(filters: string[] = []) { 171 + const { dir, root } = this.config 172 + const { include, exclude } = this.config.typecheck 173 + const testsFilesList = this.filterFiles(await this.globFiles(include, exclude, dir || root), filters) 174 + const checker = new Typechecker(this, testsFilesList) 175 + this.typechecker = checker 176 + checker.onParseEnd(async ({ files, sourceErrors }) => { 177 + this.ctx.state.collectFiles(checker.getTestFiles()) 178 + await this.report('onTaskUpdate', checker.getTestPacks()) 179 + await this.report('onCollected') 180 + if (!files.length) { 181 + this.ctx.logger.printNoTestFound() 182 + } 183 + else { 184 + if (hasFailed(files)) 185 + process.exitCode = 1 186 + await this.report('onFinished', files) 187 + } 188 + if (sourceErrors.length && !this.config.typecheck.ignoreSourceErrors) { 189 + process.exitCode = 1 190 + await this.ctx.logger.printSourceTypeErrors(sourceErrors) 191 + } 192 + // if there are source errors, we are showing it, and then terminating process 193 + if (!files.length) { 194 + const exitCode = this.config.passWithNoTests ? (process.exitCode ?? 0) : 1 195 + process.exit(exitCode) 196 + } 197 + if (this.config.watch) { 198 + await this.report('onWatcherStart', files, [ 199 + ...(this.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), 200 + ...this.ctx.state.getUnhandledErrors(), 201 + ]) 202 + } 203 + }) 204 + checker.onParseStart(async () => { 205 + await this.report('onInit', this.ctx) 206 + this.ctx.state.collectFiles(checker.getTestFiles()) 207 + await this.report('onCollected') 208 + }) 209 + checker.onWatcherRerun(async () => { 210 + await this.report('onWatcherRerun', testsFilesList, 'File change detected. Triggering rerun.') 211 + await checker.collectTests() 212 + this.ctx.state.collectFiles(checker.getTestFiles()) 213 + await this.report('onTaskUpdate', checker.getTestPacks()) 214 + await this.report('onCollected') 215 + }) 216 + await checker.prepare() 217 + await checker.collectTests() 218 + await checker.start() 219 + } 220 + 221 + isBrowserEnabled() { 222 + return isBrowserEnabled(this.config) 223 + } 224 + 225 + getSerializableConfig() { 226 + return deepMerge({ 227 + ...this.config, 228 + coverage: this.ctx.config.coverage, 229 + reporters: [], 230 + deps: { 231 + ...this.config.deps, 232 + experimentalOptimizer: { 233 + enabled: this.config.deps?.experimentalOptimizer?.enabled ?? false, 234 + }, 235 + }, 236 + snapshotOptions: { 237 + ...this.config.snapshotOptions, 238 + resolveSnapshotPath: undefined, 239 + }, 240 + onConsoleLog: undefined!, 241 + sequence: { 242 + ...this.ctx.config.sequence, 243 + sequencer: undefined!, 244 + }, 245 + benchmark: { 246 + ...this.config.benchmark, 247 + reporters: [], 248 + }, 249 + inspect: this.ctx.config.inspect, 250 + inspectBrk: this.ctx.config.inspectBrk, 251 + }, this.ctx.configOverride || {} as any, 252 + ) as ResolvedConfig 253 + } 254 + 255 + close() { 256 + if (!this.closingPromise) { 257 + this.closingPromise = Promise.all([ 258 + this.server.close(), 259 + this.typechecker?.stop(), 260 + this.browser?.close(), 261 + ].filter(Boolean)) 262 + } 263 + return this.closingPromise 264 + } 265 + 266 + async initBrowserProvider() { 267 + if (!this.isBrowserEnabled()) 268 + return 269 + if (this.browserProvider) 270 + return 271 + const Provider = await getBrowserProvider(this.config.browser, this.runner) 272 + this.browserProvider = new Provider() 273 + const browser = this.config.browser.name 274 + const supportedBrowsers = this.browserProvider.getSupportedBrowsers() 275 + if (!browser) 276 + throw new Error(`[${this.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`) 277 + if (!supportedBrowsers.includes(browser)) 278 + throw new Error(`[${this.getName()}] Browser "${browser}" is not supported by the browser provider "${this.browserProvider.name}". Supported browsers: ${supportedBrowsers.join(', ')}.`) 279 + await this.browserProvider.initialize(this, { browser }) 280 + } 281 + }
+4
packages/vitest/src/runtime/child.ts
··· 22 22 moduleCache, 23 23 config, 24 24 mockMap, 25 + durations: { 26 + environment: 0, 27 + prepare: performance.now(), 28 + }, 25 29 rpc: createBirpc<RuntimeRPC>( 26 30 {}, 27 31 {
+16 -2
packages/vitest/src/runtime/entry.ts
··· 51 51 52 52 const originalOnCollected = testRunner.onCollected 53 53 testRunner.onCollected = async (files) => { 54 + const state = getWorkerState() 55 + files.forEach((file) => { 56 + file.prepareDuration = state.durations.prepare 57 + file.environmentLoad = state.durations.environment 58 + // should be collected only for a single test file in a batch 59 + state.durations.prepare = 0 60 + state.durations.environment = 0 61 + }) 54 62 rpc().onCollected(files) 55 63 await originalOnCollected?.call(testRunner, files) 56 64 } ··· 67 75 68 76 // browser shouldn't call this! 69 77 export async function run(files: string[], config: ResolvedConfig, environment: ContextTestEnvironment, executor: VitestExecutor): Promise<void> { 78 + const workerState = getWorkerState() 79 + 70 80 await setupGlobalEnv(config) 71 81 await startCoverageInsideWorker(config.coverage, executor) 72 - 73 - const workerState = getWorkerState() 74 82 75 83 if (config.chaiConfig) 76 84 setupChaiConfig(config.chaiConfig) 77 85 78 86 const runner = await getTestRunner(config, executor) 79 87 88 + workerState.durations.prepare = performance.now() - workerState.durations.prepare 89 + 80 90 // @ts-expect-error untyped global 81 91 globalThis.__vitest_environment__ = environment 82 92 93 + workerState.durations.environment = performance.now() 94 + 83 95 await withEnv(environment.name, environment.options || config.environmentOptions || {}, executor, async () => { 96 + workerState.durations.environment = performance.now() - workerState.durations.environment 97 + 84 98 for (const file of files) { 85 99 // it doesn't matter if running with --threads 86 100 // if running with --no-threads, we usually want to reset everything before running a test
+4 -4
packages/vitest/src/runtime/execute.ts
··· 48 48 function catchError(err: unknown, type: string) { 49 49 const worker = getWorkerState() 50 50 const error = processError(err) 51 - if (worker.filepath && !isPrimitive(error)) { 51 + if (!isPrimitive(error)) { 52 52 error.VITEST_TEST_NAME = worker.current?.name 53 - error.VITEST_TEST_PATH = relative(config.root, worker.filepath) 53 + if (worker.filepath) 54 + error.VITEST_TEST_PATH = relative(config.root, worker.filepath) 55 + error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun 54 56 } 55 - error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun 56 - 57 57 rpc().onUnhandledError(error, type) 58 58 } 59 59
+4
packages/vitest/src/runtime/worker.ts
··· 24 24 moduleCache, 25 25 config, 26 26 mockMap, 27 + durations: { 28 + environment: 0, 29 + prepare: performance.now(), 30 + }, 27 31 rpc: createBirpc<RuntimeRPC>( 28 32 {}, 29 33 {
+4 -3
packages/vitest/src/typecheck/collect.ts
··· 4 4 import type { RawSourceMap } from 'vite-node' 5 5 6 6 import { calculateSuiteHash, generateHash, interpretTaskModes, someTasksAreOnly } from '@vitest/runner/utils' 7 - import type { File, Suite, Test, Vitest } from '../types' 7 + import type { File, Suite, Test } from '../types' 8 + import type { WorkspaceProject } from '../node/workspace' 8 9 9 10 interface ParsedFile extends File { 10 11 start: number ··· 38 39 definitions: LocalCallDefinition[] 39 40 } 40 41 41 - export async function collectTests(ctx: Vitest, filepath: string): Promise<null | FileInformation> { 42 + export async function collectTests(ctx: WorkspaceProject, filepath: string): Promise<null | FileInformation> { 42 43 const request = await ctx.vitenode.transformRequest(filepath) 43 44 if (!request) 44 45 return null ··· 50 51 const file: ParsedFile = { 51 52 filepath, 52 53 type: 'suite', 53 - id: generateHash(testFilepath), 54 + id: generateHash(`${testFilepath}${ctx.config.name || ''}`), 54 55 name: testFilepath, 55 56 mode: 'run', 56 57 tasks: [],
+3 -2
packages/vitest/src/typecheck/typechecker.ts
··· 5 5 import { SourceMapConsumer } from 'source-map' 6 6 import { getTasks } from '../utils' 7 7 import { ensurePackageInstalled } from '../node/pkg' 8 - import type { Awaitable, File, ParsedStack, Task, TaskResultPack, TaskState, TscErrorInfo, Vitest } from '../types' 8 + import type { Awaitable, File, ParsedStack, Task, TaskResultPack, TaskState, TscErrorInfo } from '../types' 9 + import type { WorkspaceProject } from '../node/workspace' 9 10 import { getRawErrsMapFromTsCompile, getTsconfig } from './parse' 10 11 import { createIndexMap } from './utils' 11 12 import type { FileInformation } from './collect' ··· 40 41 private allowJs?: boolean 41 42 private process!: ExecaChildProcess 42 43 43 - constructor(protected ctx: Vitest, protected files: string[]) { } 44 + constructor(protected ctx: WorkspaceProject, protected files: string[]) { } 44 45 45 46 public onParseStart(fn: Callback) { 46 47 this._onParseStart = fn
+2 -2
packages/vitest/src/types/browser.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 - import type { Vitest } from '../node' 2 + import type { WorkspaceProject } from '../node/workspace' 3 3 import type { ApiConfig } from './config' 4 4 5 5 export interface BrowserProviderOptions { ··· 9 9 export interface BrowserProvider { 10 10 name: string 11 11 getSupportedBrowsers(): readonly string[] 12 - initialize(ctx: Vitest, options: BrowserProviderOptions): Awaitable<void> 12 + initialize(ctx: WorkspaceProject, options: BrowserProviderOptions): Awaitable<void> 13 13 openPage(url: string): Awaitable<void> 14 14 close(): Awaitable<void> 15 15 }
+124 -81
packages/vitest/src/types/config.ts
··· 35 35 36 36 export type VitestRunMode = 'test' | 'benchmark' | 'typecheck' 37 37 38 + interface SequenceOptions { 39 + /** 40 + * Class that handles sorting and sharding algorithm. 41 + * If you only need to change sorting, you can extend 42 + * your custom sequencer from `BaseSequencer` from `vitest/node`. 43 + * @default BaseSequencer 44 + */ 45 + sequencer?: TestSequencerConstructor 46 + /** 47 + * Should tests run in random order. 48 + * @default false 49 + */ 50 + shuffle?: boolean 51 + /** 52 + * Defines how setup files should be ordered 53 + * - 'parallel' will run all setup files in parallel 54 + * - 'list' will run all setup files in the order they are defined in the config file 55 + * @default 'parallel' 56 + */ 57 + setupFiles?: SequenceSetupFiles 58 + /** 59 + * Seed for the random number generator. 60 + * @default Date.now() 61 + */ 62 + seed?: number 63 + /** 64 + * Defines how hooks should be ordered 65 + * - `stack` will order "after" hooks in reverse order, "before" hooks will run sequentially 66 + * - `list` will order hooks in the order they are defined 67 + * - `parallel` will run hooks in a single group in parallel 68 + * @default 'parallel' 69 + */ 70 + hooks?: SequenceHooks 71 + } 72 + 73 + interface DepsOptions { 74 + /** 75 + * Enable dependency optimization. This can improve the performance of your tests. 76 + */ 77 + experimentalOptimizer?: Omit<DepOptimizationConfig, 'disabled'> & { 78 + enabled: boolean 79 + } 80 + /** 81 + * Externalize means that Vite will bypass the package to native Node. 82 + * 83 + * Externalized dependencies will not be applied Vite's transformers and resolvers. 84 + * And does not support HMR on reload. 85 + * 86 + * Typically, packages under `node_modules` are externalized. 87 + */ 88 + external?: (string | RegExp)[] 89 + /** 90 + * Vite will process inlined modules. 91 + * 92 + * This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle). 93 + * 94 + * If `true`, every dependency will be inlined 95 + */ 96 + inline?: (string | RegExp)[] | true 97 + 98 + /** 99 + * Interpret CJS module's default as named exports 100 + * 101 + * @default true 102 + */ 103 + interopDefault?: boolean 104 + 105 + /** 106 + * When a dependency is a valid ESM package, try to guess the cjs version based on the path. 107 + * This will significantly improve the performance in huge repo, but might potentially 108 + * cause some misalignment if a package have different logic in ESM and CJS mode. 109 + * 110 + * @default false 111 + */ 112 + fallbackCJS?: boolean 113 + 114 + /** 115 + * Use experimental Node loader to resolve imports inside node_modules using Vite resolve algorithm. 116 + * @default false 117 + */ 118 + registerNodeLoader?: boolean 119 + } 120 + 38 121 export interface InlineConfig { 39 122 /** 40 123 * Name of the project. Will be used to display in the reporter. ··· 71 154 /** 72 155 * Handling for dependencies inlining or externalizing 73 156 */ 74 - deps?: { 75 - /** 76 - * Enable dependency optimization. This can improve the performance of your tests. 77 - */ 78 - experimentalOptimizer?: Omit<DepOptimizationConfig, 'disabled'> & { 79 - enabled: boolean 80 - } 81 - /** 82 - * Externalize means that Vite will bypass the package to native Node. 83 - * 84 - * Externalized dependencies will not be applied Vite's transformers and resolvers. 85 - * And does not support HMR on reload. 86 - * 87 - * Typically, packages under `node_modules` are externalized. 88 - */ 89 - external?: (string | RegExp)[] 90 - /** 91 - * Vite will process inlined modules. 92 - * 93 - * This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle). 94 - * 95 - * If `true`, every dependency will be inlined 96 - */ 97 - inline?: (string | RegExp)[] | true 98 - 99 - /** 100 - * Interpret CJS module's default as named exports 101 - * 102 - * @default true 103 - */ 104 - interopDefault?: boolean 105 - 106 - /** 107 - * When a dependency is a valid ESM package, try to guess the cjs version based on the path. 108 - * This will significantly improve the performance in huge repo, but might potentially 109 - * cause some misalignment if a package have different logic in ESM and CJS mode. 110 - * 111 - * @default false 112 - */ 113 - fallbackCJS?: boolean 114 - 115 - /** 116 - * Use experimental Node loader to resolve imports inside node_modules using Vite resolve algorithm. 117 - * @default false 118 - */ 119 - registerNodeLoader?: boolean 120 - } 157 + deps?: DepsOptions 121 158 122 159 /** 123 160 * Base directory to scan for the test files ··· 480 517 /** 481 518 * Options for configuring the order of running tests. 482 519 */ 483 - sequence?: { 484 - /** 485 - * Class that handles sorting and sharding algorithm. 486 - * If you only need to change sorting, you can extend 487 - * your custom sequencer from `BaseSequencer` from `vitest/node`. 488 - * @default BaseSequencer 489 - */ 490 - sequencer?: TestSequencerConstructor 491 - /** 492 - * Should tests run in random order. 493 - * @default false 494 - */ 495 - shuffle?: boolean 496 - /** 497 - * Defines how setup files should be ordered 498 - * - 'parallel' will run all setup files in parallel 499 - * - 'list' will run all setup files in the order they are defined in the config file 500 - * @default 'parallel' 501 - */ 502 - setupFiles?: SequenceSetupFiles 503 - /** 504 - * Seed for the random number generator. 505 - * @default Date.now() 506 - */ 507 - seed?: number 508 - /** 509 - * Defines how hooks should be ordered 510 - * - `stack` will order "after" hooks in reverse order, "before" hooks will run sequentially 511 - * - `list` will order hooks in the order they are defined 512 - * - `parallel` will run hooks in a single group in parallel 513 - * @default 'parallel' 514 - */ 515 - hooks?: SequenceHooks 516 - } 520 + sequence?: SequenceOptions 517 521 518 522 /** 519 523 * Specifies an `Object`, or an `Array` of `Object`, ··· 681 685 runner?: string 682 686 } 683 687 688 + export type ProjectConfig = Omit< 689 + UserConfig, 690 + | 'sequencer' 691 + | 'shard' 692 + | 'watch' 693 + | 'run' 694 + | 'cache' 695 + | 'update' 696 + | 'reporters' 697 + | 'outputFile' 698 + | 'maxThreads' 699 + | 'minThreads' 700 + | 'useAtomics' 701 + | 'teardownTimeout' 702 + | 'silent' 703 + | 'watchExclude' 704 + | 'forceRerunTriggers' 705 + | 'testNamePattern' 706 + | 'ui' 707 + | 'open' 708 + | 'uiBase' 709 + // TODO: allow snapshot options 710 + | 'snapshotFormat' 711 + | 'resolveSnapshotPath' 712 + | 'passWithNoTests' 713 + | 'onConsoleLog' 714 + | 'dangerouslyIgnoreUnhandledErrors' 715 + | 'slowTestThreshold' 716 + | 'inspect' 717 + | 'inspectBrk' 718 + | 'deps' 719 + | 'coverage' 720 + > & { 721 + sequencer?: Omit<SequenceOptions, 'sequencer' | 'seed'> 722 + deps?: Omit<DepsOptions, 'registerNodeLoader'> 723 + } 724 + 684 725 export type RuntimeConfig = Pick< 685 726 UserConfig, 686 727 | 'allowOnly' ··· 692 733 | 'fakeTimers' 693 734 | 'maxConcurrency' 694 735 > & { sequence?: { hooks?: SequenceHooks } } 736 + 737 + export type { UserWorkspaceConfig } from '../config'
+5
packages/vitest/src/types/global.ts
··· 26 26 expect: Vi.ExpectStatic 27 27 } 28 28 29 + interface File { 30 + prepareDuration?: number 31 + environmentLoad?: number 32 + } 33 + 29 34 interface TaskBase { 30 35 logs?: UserConsoleLog[] 31 36 }
+4
packages/vitest/src/types/worker.ts
··· 26 26 environmentTeardownRun?: boolean 27 27 moduleCache: ModuleCacheMap 28 28 mockMap: MockMap 29 + durations: { 30 + environment: number 31 + prepare: number 32 + } 29 33 }
+1 -1
packages/vitest/src/utils/graph.ts
··· 29 29 graph[id] = (await Promise.all(mods.map(m => get(m, seen)))).filter(Boolean) as string[] 30 30 return id 31 31 } 32 - await get(ctx.server.moduleGraph.getModuleById(id) || ctx.browser?.moduleGraph.getModuleById(id)) 32 + await get(ctx.server.moduleGraph.getModuleById(id)) 33 33 return { 34 34 graph, 35 35 externalized: Array.from(externalized),
+8 -6
packages/vitest/src/utils/test-helpers.ts
··· 1 1 import { promises as fs } from 'node:fs' 2 2 import mm from 'micromatch' 3 - import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from '../types' 3 + import type { EnvironmentOptions, VitestEnvironment } from '../types' 4 + import type { WorkspaceProject } from '../node/workspace' 4 5 import { groupBy } from './base' 5 6 6 7 export const envsOrder = [ ··· 16 17 envOptions: EnvironmentOptions | null 17 18 } 18 19 19 - export async function groupFilesByEnv(files: string[], config: ResolvedConfig) { 20 - const filesWithEnv = await Promise.all(files.map(async (file) => { 20 + export async function groupFilesByEnv(files: (readonly [WorkspaceProject, string])[]) { 21 + const filesWithEnv = await Promise.all(files.map(async ([project, file]) => { 21 22 const code = await fs.readFile(file, 'utf-8') 22 23 23 24 // 1. Check for control comments in the file 24 25 let env = code.match(/@(?:vitest|jest)-environment\s+?([\w-]+)\b/)?.[1] 25 26 // 2. Check for globals 26 27 if (!env) { 27 - for (const [glob, target] of config.environmentMatchGlobs || []) { 28 - if (mm.isMatch(file, glob, { cwd: config.root })) { 28 + for (const [glob, target] of project.config.environmentMatchGlobs || []) { 29 + if (mm.isMatch(file, glob, { cwd: project.config.root })) { 29 30 env = target 30 31 break 31 32 } 32 33 } 33 34 } 34 35 // 3. Fallback to global env 35 - env ||= config.environment || 'node' 36 + env ||= project.config.environment || 'node' 36 37 37 38 const envOptions = JSON.parse(code.match(/@(?:vitest|jest)-environment-options\s+?(.+)/)?.[1] || 'null') 38 39 return { 39 40 file, 41 + project, 40 42 environment: { 41 43 name: env as VitestEnvironment, 42 44 options: envOptions ? { [env]: envOptions } as EnvironmentOptions : null,
+4
test/reporters/tests/__snapshots__/html.test.ts.snap
··· 6 6 "files": [ 7 7 { 8 8 "collectDuration": 0, 9 + "environmentLoad": 0, 9 10 "filepath": "<rootDir>/test/reporters/fixtures/json-fail.test.ts", 10 11 "id": 0, 11 12 "mode": "run", 12 13 "name": "json-fail.test.ts", 14 + "prepareDuration": 0, 13 15 "result": { 14 16 "duration": 0, 15 17 "hooks": { ··· 125 127 "files": [ 126 128 { 127 129 "collectDuration": 0, 130 + "environmentLoad": 0, 128 131 "filepath": "<rootDir>/test/reporters/fixtures/all-passing-or-skipped.test.ts", 129 132 "id": 0, 130 133 "mode": "run", 131 134 "name": "all-passing-or-skipped.test.ts", 135 + "prepareDuration": 0, 132 136 "result": { 133 137 "duration": 0, 134 138 "hooks": {
+5
test/workspaces/space_1/test/happy-dom.spec.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('window is defined', () => { 4 + expect(window).toBeDefined() 5 + })
+6
test/workspaces/space_1/test/math.spec.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { sum } from '../../src/math' 3 + 4 + test('3 + 3 = 6', () => { 5 + expect(sum(3, 3)).toBe(6) 6 + })
+5
test/workspaces/space_2/test/node.spec.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('window is not defined', () => { 4 + expect(typeof window).toBe('undefined') 5 + })
+3
test/workspaces/space_3/src/multiply.ts
··· 1 + export function multiple(a: number, b: number) { 2 + return a * b 3 + }
+8 -19
packages/vitest/src/integrations/browser/server.ts
··· 1 1 import { createServer } from 'vite' 2 2 import { resolve } from 'pathe' 3 3 import { findUp } from 'find-up' 4 - import { configFiles } from '../../constants' 5 - import type { Vitest } from '../../node' 4 + import { configFiles, defaultBrowserPort } from '../../constants' 6 5 import type { UserConfig } from '../../types/config' 7 6 import { ensurePackageInstalled } from '../../node/pkg' 8 7 import { resolveApiServerConfig } from '../../node/config' 9 8 import { CoverageTransform } from '../../node/plugins/coverageTransform' 9 + import type { WorkspaceProject } from '../../node/workspace' 10 10 11 - export async function createBrowserServer(ctx: Vitest, options: UserConfig) { 12 - const root = ctx.config.root 11 + export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) { 12 + const root = project.config.root 13 13 14 14 await ensurePackageInstalled('@vitest/browser', root) 15 15 ··· 21 21 22 22 const server = await createServer({ 23 23 logLevel: 'error', 24 - mode: ctx.config.mode, 24 + mode: project.config.mode, 25 25 configFile: configPath, 26 26 // watch is handled by Vitest 27 27 server: { ··· 32 32 }, 33 33 plugins: [ 34 34 (await import('@vitest/browser')).default('/'), 35 - CoverageTransform(ctx), 35 + CoverageTransform(project.ctx), 36 36 { 37 37 enforce: 'post', 38 38 name: 'vitest:browser:config', 39 39 async config(config) { 40 40 const server = resolveApiServerConfig(config.test?.browser || {}) || { 41 - port: 63315, 41 + port: defaultBrowserPort, 42 42 } 43 43 44 44 config.server = server 45 45 config.server.fs = { strict: false } 46 - 47 - config.optimizeDeps ??= {} 48 - config.optimizeDeps.entries ??= [] 49 - 50 - const [...entries] = await ctx.globAllTestFiles(ctx.config, ctx.config.dir || root) 51 - entries.push(...ctx.config.setupFiles) 52 - 53 - if (typeof config.optimizeDeps.entries === 'string') 54 - config.optimizeDeps.entries = [config.optimizeDeps.entries] 55 - 56 - config.optimizeDeps.entries.push(...entries) 57 46 58 47 return { 59 48 resolve: { ··· 68 57 await server.listen() 69 58 await server.watcher.close() 70 59 71 - ;(await import('../../api/setup')).setup(ctx, server) 60 + ;(await import('../../api/setup')).setup(project, server) 72 61 73 62 return server 74 63 }
+3 -3
packages/vitest/src/node/browser/playwright.ts
··· 1 1 import type { Page } from 'playwright' 2 2 import type { BrowserProvider, BrowserProviderOptions } from '../../types/browser' 3 3 import { ensurePackageInstalled } from '../pkg' 4 - import type { Vitest } from '../core' 4 + import type { WorkspaceProject } from '../workspace' 5 5 6 6 export const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const 7 7 export type PlaywrightBrowser = typeof playwrightBrowsers[number] ··· 15 15 16 16 private cachedBrowser: Page | null = null 17 17 private browser!: PlaywrightBrowser 18 - private ctx!: Vitest 18 + private ctx!: WorkspaceProject 19 19 20 20 getSupportedBrowsers() { 21 21 return playwrightBrowsers 22 22 } 23 23 24 - async initialize(ctx: Vitest, { browser }: PlaywrightProviderOptions) { 24 + async initialize(ctx: WorkspaceProject, { browser }: PlaywrightProviderOptions) { 25 25 this.ctx = ctx 26 26 this.browser = browser 27 27
+3 -3
packages/vitest/src/node/browser/webdriver.ts
··· 1 1 import type { Browser } from 'webdriverio' 2 2 import type { BrowserProvider, BrowserProviderOptions } from '../../types/browser' 3 3 import { ensurePackageInstalled } from '../pkg' 4 - import type { Vitest } from '../core' 4 + import type { WorkspaceProject } from '../workspace' 5 5 6 6 export const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const 7 7 export type WebdriverBrowser = typeof webdriverBrowsers[number] ··· 16 16 private cachedBrowser: Browser | null = null 17 17 private stopSafari: () => void = () => {} 18 18 private browser!: WebdriverBrowser 19 - private ctx!: Vitest 19 + private ctx!: WorkspaceProject 20 20 21 21 getSupportedBrowsers() { 22 22 return webdriverBrowsers 23 23 } 24 24 25 - async initialize(ctx: Vitest, { browser }: WebdriverProviderOptions) { 25 + async initialize(ctx: WorkspaceProject, { browser }: WebdriverProviderOptions) { 26 26 this.ctx = ctx 27 27 this.browser = browser 28 28
+18 -5
packages/vitest/src/node/cache/files.ts
··· 1 1 import fs from 'node:fs' 2 2 import type { Stats } from 'node:fs' 3 + import { relative } from 'pathe' 4 + import type { WorkspaceSpec } from '../pool' 3 5 4 6 type FileStatsCache = Pick<Stats, 'size'> 5 7 6 8 export class FilesStatsCache { 7 9 public cache = new Map<string, FileStatsCache>() 8 10 9 - public getStats(fsPath: string): FileStatsCache | undefined { 10 - return this.cache.get(fsPath) 11 + public getStats(key: string): FileStatsCache | undefined { 12 + return this.cache.get(key) 11 13 } 12 14 13 - public async updateStats(fsPath: string) { 15 + public async populateStats(root: string, specs: WorkspaceSpec[]) { 16 + const promises = specs.map((spec) => { 17 + const key = `${spec[0].getName()}:${relative(root, spec[1])}` 18 + return this.updateStats(spec[1], key) 19 + }) 20 + await Promise.all(promises) 21 + } 22 + 23 + public async updateStats(fsPath: string, key: string) { 14 24 if (!fs.existsSync(fsPath)) 15 25 return 16 26 const stats = await fs.promises.stat(fsPath) 17 - this.cache.set(fsPath, { size: stats.size }) 27 + this.cache.set(key, { size: stats.size }) 18 28 } 19 29 20 30 public removeStats(fsPath: string) { 21 - this.cache.delete(fsPath) 31 + this.cache.forEach((_, key) => { 32 + if (key.endsWith(fsPath)) 33 + this.cache.delete(key) 34 + }) 22 35 } 23 36 }
+4 -4
packages/vitest/src/node/cache/index.ts
··· 12 12 results = new ResultsCache() 13 13 stats = new FilesStatsCache() 14 14 15 - getFileTestResults(id: string) { 16 - return this.results.getResults(id) 15 + getFileTestResults(key: string) { 16 + return this.results.getResults(key) 17 17 } 18 18 19 - getFileStats(id: string) { 20 - return this.stats.getStats(id) 19 + getFileStats(key: string) { 20 + return this.stats.getStats(key) 21 21 } 22 22 23 23 static resolveCacheDir(root: string, dir: string | undefined) {
+23 -9
packages/vitest/src/node/cache/results.ts
··· 1 1 import fs from 'node:fs' 2 - import { dirname, resolve } from 'pathe' 2 + import { dirname, relative, resolve } from 'pathe' 3 3 import type { File, ResolvedConfig } from '../../types' 4 4 import { version } from '../../../package.json' 5 5 ··· 10 10 11 11 export class ResultsCache { 12 12 private cache = new Map<string, SuiteResultCache>() 13 + private workspacesKeyMap = new Map<string, string[]>() 13 14 private cachePath: string | null = null 14 15 private version: string = version 15 16 private root = '/' ··· 24 25 this.cachePath = resolve(config.dir, 'results.json') 25 26 } 26 27 27 - getResults(fsPath: string) { 28 - return this.cache.get(fsPath?.slice(this.root.length)) 28 + getResults(key: string) { 29 + return this.cache.get(key) 29 30 } 30 31 31 32 async readFromCache() { 32 33 if (!this.cachePath) 33 34 return 34 35 35 - if (fs.existsSync(this.cachePath)) { 36 - const resultsCache = await fs.promises.readFile(this.cachePath, 'utf8') 37 - const { results, version } = JSON.parse(resultsCache) 36 + if (!fs.existsSync(this.cachePath)) 37 + return 38 + 39 + const resultsCache = await fs.promises.readFile(this.cachePath, 'utf8') 40 + const { results, version } = JSON.parse(resultsCache || '[]') 41 + // handling changed in 0.30.0 42 + if (Number(version.split('.')[1]) >= 30) { 38 43 this.cache = new Map(results) 39 44 this.version = version 45 + results.forEach(([spec]: [string]) => { 46 + const [projectName, relativePath] = spec.split(':') 47 + const keyMap = this.workspacesKeyMap.get(relativePath) || [] 48 + keyMap.push(projectName) 49 + this.workspacesKeyMap.set(relativePath, keyMap) 50 + }) 40 51 } 41 52 } 42 53 ··· 47 58 return 48 59 const duration = result.duration || 0 49 60 // store as relative, so cache would be the same in CI and locally 50 - const relativePath = file.filepath?.slice(this.root.length) 51 - this.cache.set(relativePath, { 61 + const relativePath = relative(this.root, file.filepath) 62 + this.cache.set(`${file.projectName || ''}:${relativePath}`, { 52 63 duration: duration >= 0 ? duration : 0, 53 64 failed: result.state === 'fail', 54 65 }) ··· 56 67 } 57 68 58 69 removeFromCache(filepath: string) { 59 - this.cache.delete(filepath) 70 + this.cache.forEach((_, key) => { 71 + if (key.endsWith(filepath)) 72 + this.cache.delete(key) 73 + }) 60 74 } 61 75 62 76 async writeToCache() {
+2 -3
packages/vitest/src/node/plugins/cssEnabler.ts
··· 1 1 import { relative } from 'pathe' 2 2 import type { Plugin as VitePlugin } from 'vite' 3 3 import { generateCssFilenameHash } from '../../integrations/css/css-modules' 4 - import type { CSSModuleScopeStrategy } from '../../types' 4 + import type { CSSModuleScopeStrategy, ResolvedConfig } from '../../types' 5 5 import { toArray } from '../../utils' 6 - import type { Vitest } from '../core' 7 6 8 7 const cssLangs = '\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)' 9 8 const cssLangRE = new RegExp(cssLangs) ··· 24 23 return `\`_\${style}_${hash}\`` 25 24 } 26 25 27 - export function CSSEnablerPlugin(ctx: Vitest): VitePlugin[] { 26 + export function CSSEnablerPlugin(ctx: { config: ResolvedConfig }): VitePlugin[] { 28 27 const shouldProcessCSS = (id: string) => { 29 28 const { css } = ctx.config 30 29 if (typeof css === 'boolean')
+13 -10
packages/vitest/src/node/plugins/globalSetup.ts
··· 1 1 import type { Plugin } from 'vite' 2 2 import type { ViteNodeRunner } from 'vite-node/client' 3 3 import c from 'picocolors' 4 - import type { Vitest } from '../core' 5 4 import { toArray } from '../../utils' 6 5 import { divider } from '../reporters/renderers/utils' 6 + import type { Vitest } from '../core' 7 + import type { Logger } from '../logger' 7 8 8 9 interface GlobalSetupFile { 9 10 file: string ··· 11 12 teardown?: Function 12 13 } 13 14 14 - async function loadGlobalSetupFiles(ctx: Vitest): Promise<GlobalSetupFile[]> { 15 - const server = ctx.server 16 - const runner = ctx.runner 15 + type SetupInstance = Pick<Vitest, 'runner' | 'server'> 16 + 17 + async function loadGlobalSetupFiles(project: SetupInstance): Promise<GlobalSetupFile[]> { 18 + const server = project.server 19 + const runner = project.runner 17 20 const globalSetupFiles = toArray(server.config.test?.globalSetup) 18 21 return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, runner))) 19 22 } ··· 42 45 } 43 46 } 44 47 45 - export function GlobalSetupPlugin(ctx: Vitest): Plugin { 48 + export function GlobalSetupPlugin(project: SetupInstance, logger: Logger): Plugin { 46 49 let globalSetupFiles: GlobalSetupFile[] 47 50 return { 48 51 name: 'vitest:global-setup-plugin', 49 52 enforce: 'pre', 50 53 51 54 async buildStart() { 52 - if (!ctx.server.config.test?.globalSetup) 55 + if (!project.server.config.test?.globalSetup) 53 56 return 54 57 55 - globalSetupFiles = await loadGlobalSetupFiles(ctx) 58 + globalSetupFiles = await loadGlobalSetupFiles(project) 56 59 57 60 try { 58 61 for (const globalSetupFile of globalSetupFiles) { ··· 65 68 } 66 69 } 67 70 catch (e) { 68 - ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) 69 - await ctx.logger.printError(e) 71 + logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) 72 + await logger.printError(e) 70 73 process.exit(1) 71 74 } 72 75 }, ··· 78 81 await globalSetupFile.teardown?.() 79 82 } 80 83 catch (error) { 81 - ctx.logger.error(`error during global teardown of ${globalSetupFile.file}`, error) 84 + logger.error(`error during global teardown of ${globalSetupFile.file}`, error) 82 85 } 83 86 } 84 87 }
+45 -46
packages/vitest/src/node/plugins/index.ts
··· 1 - import { builtinModules } from 'node:module' 2 1 import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' 3 - import { normalize, relative, resolve } from 'pathe' 4 - import { toArray } from '@vitest/utils' 5 - import { resolveModule } from 'local-pkg' 2 + import { relative } from 'pathe' 6 3 import { configDefaults } from '../../defaults' 7 4 import type { ResolvedConfig, UserConfig } from '../../types' 8 5 import { deepMerge, notNullish, removeUndefinedValues } from '../../utils' ··· 136 133 } 137 134 138 135 const optimizeConfig: Partial<ViteConfig> = {} 139 - const optimizer = preOptions.deps?.experimentalOptimizer 140 - if (!optimizer?.enabled) { 141 - optimizeConfig.cacheDir = undefined 142 - optimizeConfig.optimizeDeps = { 143 - // experimental in Vite >2.9.2, entries remains to help with older versions 144 - disabled: true, 145 - entries: [], 146 - } 136 + // TODO: optimizer is temporary disabled, until Vite provides "optimzier.byDefault" option 137 + // const optimizer = preOptions.deps?.experimentalOptimizer 138 + // if (!optimizer?.enabled) { 139 + optimizeConfig.cacheDir = undefined 140 + optimizeConfig.optimizeDeps = { 141 + // experimental in Vite >2.9.2, entries remains to help with older versions 142 + disabled: true, 143 + entries: [], 147 144 } 148 - else { 149 - const root = config.root || process.cwd() 150 - const [...entries] = await ctx.globAllTestFiles(preOptions as ResolvedConfig, preOptions.dir || root) 151 - if (preOptions?.setupFiles) { 152 - const setupFiles = toArray(preOptions.setupFiles).map((file: string) => 153 - normalize( 154 - resolveModule(file, { paths: [root] }) 155 - ?? resolve(root, file), 156 - ), 157 - ) 158 - entries.push(...setupFiles) 159 - } 160 - const cacheDir = preOptions.cache !== false ? preOptions.cache?.dir : null 161 - optimizeConfig.cacheDir = cacheDir ?? 'node_modules/.vitest' 162 - optimizeConfig.optimizeDeps = { 163 - ...viteConfig.optimizeDeps, 164 - ...optimizer, 165 - disabled: false, 166 - entries: [...(viteConfig.optimizeDeps?.entries || []), ...entries], 167 - exclude: ['vitest', ...builtinModules, ...(optimizer.exclude || viteConfig.optimizeDeps?.exclude || [])], 168 - include: (optimizer.include || viteConfig.optimizeDeps?.include || []).filter((n: string) => n !== 'vitest'), 169 - } 170 - // Vite throws an error that it cannot rename "deps_temp", but optimization still works 171 - // let's not show this error to users 172 - const { error: logError } = console 173 - console.error = (...args) => { 174 - if (typeof args[0] === 'string' && args[0].includes('/deps_temp')) 175 - return 176 - return logError(...args) 177 - } 178 - } 145 + // } 146 + // else { 147 + // const root = config.root || process.cwd() 148 + // // TODO: add support for experimental optimizer 149 + // const entries = [] 150 + // // const [...entries] = await ctx.globAllTestFiles(preOptions as ResolvedConfig, preOptions.dir || root) 151 + // if (preOptions?.setupFiles) { 152 + // const setupFiles = toArray(preOptions.setupFiles).map((file: string) => 153 + // normalize( 154 + // resolveModule(file, { paths: [root] }) 155 + // ?? resolve(root, file), 156 + // ), 157 + // ) 158 + // entries.push(...setupFiles) 159 + // } 160 + // const cacheDir = preOptions.cache !== false ? preOptions.cache?.dir : null 161 + // optimizeConfig.cacheDir = cacheDir ?? 'node_modules/.vitest' 162 + // optimizeConfig.optimizeDeps = { 163 + // ...viteConfig.optimizeDeps, 164 + // ...optimizer, 165 + // disabled: false, 166 + // entries: [...(viteConfig.optimizeDeps?.entries || []), ...entries], 167 + // exclude: ['vitest', ...builtinModules, ...(optimizer.exclude || viteConfig.optimizeDeps?.exclude || [])], 168 + // include: (optimizer.include || viteConfig.optimizeDeps?.include || []).filter((n: string) => n !== 'vitest'), 169 + // } 170 + // // Vite throws an error that it cannot rename "deps_temp", but optimization still works 171 + // // let's not show this error to users 172 + // const { error: logError } = console 173 + // console.error = (...args) => { 174 + // if (typeof args[0] === 'string' && args[0].includes('/deps_temp')) 175 + // return 176 + // return logError(...args) 177 + // } 178 + // } 179 179 Object.assign(config, optimizeConfig) 180 180 181 181 return config ··· 221 221 }, 222 222 async configureServer(server) { 223 223 try { 224 - await ctx.setServer(options, server) 225 - await ctx.initBrowserServer(options) 224 + await ctx.setServer(options, server, userConfig) 226 225 if (options.api && options.watch) 227 226 (await import('../../api/setup')).setup(ctx) 228 227 } ··· 237 236 }, 238 237 }, 239 238 EnvReplacerPlugin(), 240 - GlobalSetupPlugin(ctx), 239 + GlobalSetupPlugin(ctx, ctx.logger), 241 240 ...CSSEnablerPlugin(ctx), 242 241 CoverageTransform(ctx), 243 242 options.ui
+142
packages/vitest/src/node/plugins/workspace.ts
··· 1 + import { dirname, relative } from 'pathe' 2 + import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' 3 + import { configDefaults } from '../../defaults' 4 + import { generateScopedClassName } from '../../integrations/css/css-modules' 5 + import { deepMerge } from '../../utils/base' 6 + import type { WorkspaceProject } from '../workspace' 7 + import type { UserWorkspaceConfig } from '../../types' 8 + import { CoverageTransform } from './coverageTransform' 9 + import { CSSEnablerPlugin } from './cssEnabler' 10 + import { EnvReplacerPlugin } from './envReplacer' 11 + import { GlobalSetupPlugin } from './globalSetup' 12 + 13 + interface WorkspaceOptions extends UserWorkspaceConfig { 14 + root?: string 15 + workspacePath: string | number 16 + } 17 + 18 + export function WorkspaceVitestPlugin(project: WorkspaceProject, options: WorkspaceOptions) { 19 + return <VitePlugin[]>[ 20 + { 21 + name: 'vitest:project', 22 + enforce: 'pre', 23 + options() { 24 + this.meta.watchMode = false 25 + }, 26 + // TODO: refactor so we don't have the same code here and in plugins/index.ts 27 + config(viteConfig) { 28 + if (viteConfig.define) { 29 + delete viteConfig.define['import.meta.vitest'] 30 + delete viteConfig.define['process.env'] 31 + } 32 + 33 + const env: Record<string, any> = {} 34 + 35 + for (const key in viteConfig.define) { 36 + const val = viteConfig.define[key] 37 + let replacement: any 38 + try { 39 + replacement = typeof val === 'string' ? JSON.parse(val) : val 40 + } 41 + catch { 42 + // probably means it contains reference to some variable, 43 + // like this: "__VAR__": "process.env.VAR" 44 + continue 45 + } 46 + if (key.startsWith('import.meta.env.')) { 47 + const envKey = key.slice('import.meta.env.'.length) 48 + env[envKey] = replacement 49 + delete viteConfig.define[key] 50 + } 51 + else if (key.startsWith('process.env.')) { 52 + const envKey = key.slice('process.env.'.length) 53 + env[envKey] = replacement 54 + delete viteConfig.define[key] 55 + } 56 + } 57 + 58 + const testConfig = viteConfig.test || {} 59 + 60 + const root = testConfig.root || viteConfig.root || options.root 61 + let name = testConfig.name 62 + if (!name) { 63 + if (typeof options.workspacePath === 'string') 64 + name = dirname(options.workspacePath).split('/').pop() 65 + else 66 + name = options.workspacePath.toString() 67 + } 68 + 69 + const config: ViteConfig = { 70 + root, 71 + resolve: { 72 + // by default Vite resolves `module` field, which not always a native ESM module 73 + // setting this option can bypass that and fallback to cjs version 74 + mainFields: [], 75 + alias: testConfig.alias, 76 + conditions: ['node'], 77 + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 78 + // @ts-ignore we support Vite ^3.0, but browserField is available in Vite ^3.2 79 + browserField: false, 80 + }, 81 + esbuild: { 82 + sourcemap: 'external', 83 + 84 + // Enables using ignore hint for coverage providers with @preserve keyword 85 + legalComments: 'inline', 86 + }, 87 + server: { 88 + // disable watch mode in workspaces, 89 + // because it is handled by the top-level watcher 90 + watch: { 91 + ignored: ['**/*'], 92 + depth: 0, 93 + persistent: false, 94 + }, 95 + open: false, 96 + hmr: false, 97 + preTransformRequests: false, 98 + }, 99 + test: { 100 + env, 101 + name, 102 + }, 103 + } 104 + 105 + const classNameStrategy = (typeof testConfig.css !== 'boolean' && testConfig.css?.modules?.classNameStrategy) || 'stable' 106 + 107 + if (classNameStrategy !== 'scoped') { 108 + config.css ??= {} 109 + config.css.modules ??= {} 110 + if (config.css.modules) { 111 + config.css.modules.generateScopedName = (name: string, filename: string) => { 112 + const root = project.config.root 113 + return generateScopedClassName(classNameStrategy, name, relative(root, filename))! 114 + } 115 + } 116 + } 117 + 118 + return config 119 + }, 120 + async configureServer(server) { 121 + try { 122 + const options = deepMerge( 123 + {}, 124 + configDefaults, 125 + server.config.test || {}, 126 + ) 127 + await project.setServer(options, server) 128 + } 129 + catch (err) { 130 + await project.ctx.logger.printError(err, true) 131 + process.exit(1) 132 + } 133 + 134 + await server.watcher.close() 135 + }, 136 + }, 137 + EnvReplacerPlugin(), 138 + ...CSSEnablerPlugin(project), 139 + CoverageTransform(project.ctx), 140 + GlobalSetupPlugin(project, project.ctx.logger), 141 + ] 142 + }
+25 -7
packages/vitest/src/node/pools/browser.ts
··· 2 2 import { relative } from 'pathe' 3 3 import type { Vitest } from '../core' 4 4 import type { ProcessPool } from '../pool' 5 + import type { WorkspaceProject } from '../workspace' 6 + import type { BrowserProvider } from '../../types/browser' 5 7 6 8 export function createBrowserPool(ctx: Vitest): ProcessPool { 7 - const provider = ctx.browserProvider! 8 - const origin = `http://${ctx.config.browser.api?.host || 'localhost'}:${ctx.browser.config.server.port}` 9 + const providers = new Set<BrowserProvider>() 9 10 10 11 const waitForTest = (id: string) => { 11 12 const defer = createDefer() ··· 13 14 return defer 14 15 } 15 16 16 - const runTests = async (files: string[]) => { 17 - const paths = files.map(file => relative(ctx.config.root, file)) 17 + const runTests = async (project: WorkspaceProject, files: string[]) => { 18 + const provider = project.browserProvider! 19 + providers.add(provider) 18 20 19 - const isolate = ctx.config.isolate 21 + const origin = `http://${ctx.config.browser.api?.host || 'localhost'}:${project.browser.config.server.port}` 22 + const paths = files.map(file => relative(project.config.root, file)) 23 + 24 + const isolate = project.config.isolate 20 25 if (isolate) { 21 26 for (const path of paths) { 22 27 const url = new URL('/', origin) ··· 35 40 } 36 41 } 37 42 43 + const runWorkspaceTests = async (specs: [WorkspaceProject, string][]) => { 44 + const groupedFiles = new Map<WorkspaceProject, string[]>() 45 + for (const [project, file] of specs) { 46 + const files = groupedFiles.get(project) || [] 47 + files.push(file) 48 + groupedFiles.set(project, files) 49 + } 50 + 51 + for (const [project, files] of groupedFiles.entries()) 52 + await runTests(project, files) 53 + } 54 + 38 55 return { 39 56 async close() { 40 57 ctx.state.browserTestPromises.clear() 41 - await provider.close() 58 + await Promise.all([...providers].map(provider => provider.close())) 59 + providers.clear() 42 60 }, 43 - runTests, 61 + runTests: runWorkspaceTests, 44 62 } 45 63 }
+25 -15
packages/vitest/src/node/pools/child.ts
··· 4 4 import { fileURLToPath, pathToFileURL } from 'node:url' 5 5 import { createBirpc } from 'birpc' 6 6 import { resolve } from 'pathe' 7 - import type { ContextTestEnvironment, ResolvedConfig, RuntimeRPC } from '../../types' 8 - import type { Vitest } from '../core' 7 + import type { ContextTestEnvironment, ResolvedConfig, RuntimeRPC, Vitest } from '../../types' 9 8 import type { ChildContext } from '../../types/child' 10 - import type { PoolProcessOptions, ProcessPool } from '../pool' 9 + import type { PoolProcessOptions, ProcessPool, WorkspaceSpec } from '../pool' 11 10 import { distDir } from '../../paths' 12 11 import { groupBy } from '../../utils/base' 13 12 import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' 13 + import type { WorkspaceProject } from '../workspace' 14 14 import { createMethodsRPC } from './rpc' 15 15 16 16 const childPath = fileURLToPath(pathToFileURL(resolve(distDir, './child.js')).href) 17 17 18 - function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess): void { 18 + function setupChildProcessChannel(project: WorkspaceProject, fork: ChildProcess): void { 19 19 createBirpc<{}, RuntimeRPC>( 20 - createMethodsRPC(ctx), 20 + createMethodsRPC(project), 21 21 { 22 22 serialize: v8.serialize, 23 23 deserialize: v => v8.deserialize(Buffer.from(v)), ··· 37 37 return `$$vitest:${input.toString()}` 38 38 } 39 39 40 - function getTestConfig(ctx: Vitest): ResolvedConfig { 40 + function getTestConfig(ctx: WorkspaceProject): ResolvedConfig { 41 41 const config = ctx.getSerializableConfig() 42 42 // v8 serialize does not support regex 43 43 return <ResolvedConfig>{ ··· 51 51 export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProcessOptions): ProcessPool { 52 52 const children = new Set<ChildProcess>() 53 53 54 - function runFiles(config: ResolvedConfig, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { 54 + const Sequencer = ctx.config.sequence.sequencer 55 + const sequencer = new Sequencer(ctx) 56 + 57 + function runFiles(project: WorkspaceProject, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { 58 + const config = getTestConfig(project) 59 + ctx.state.clearFiles(project, files) 60 + 55 61 const data: ChildContext = { 56 62 command: 'start', 57 63 config, ··· 65 71 env, 66 72 }) 67 73 children.add(child) 68 - setupChildProcessChannel(ctx, child) 74 + setupChildProcessChannel(project, child) 69 75 70 76 return new Promise<void>((resolve, reject) => { 71 77 child.send(data, (err) => { ··· 83 89 }) 84 90 } 85 91 86 - async function runWithFiles(files: string[], invalidates: string[] = []): Promise<void> { 87 - ctx.state.clearFiles(files) 88 - const config = getTestConfig(ctx) 92 + async function runTests(specs: WorkspaceSpec[], invalidates: string[] = []): Promise<void> { 93 + const { shard } = ctx.config 89 94 90 - const filesByEnv = await groupFilesByEnv(files, config) 95 + if (shard) 96 + specs = await sequencer.shard(specs) 97 + 98 + specs = await sequencer.sort(specs) 99 + 100 + const filesByEnv = await groupFilesByEnv(specs) 91 101 const envs = envsOrder.concat( 92 102 Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), 93 103 ) ··· 99 109 if (!files?.length) 100 110 continue 101 111 102 - const filesByOptions = groupBy(files, ({ environment }) => JSON.stringify(environment.options)) 112 + const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options)) 103 113 104 114 for (const option in filesByOptions) { 105 115 const files = filesByOptions[option] 106 116 107 117 if (files?.length) { 108 118 const filenames = files.map(f => f.file) 109 - await runFiles(config, filenames, files[0].environment, invalidates) 119 + await runFiles(files[0].project, filenames, files[0].environment, invalidates) 110 120 } 111 121 } 112 122 } 113 123 } 114 124 115 125 return { 116 - runTests: runWithFiles, 126 + runTests, 117 127 async close() { 118 128 children.forEach((child) => { 119 129 if (!child.killed)
+15 -14
packages/vitest/src/node/pools/rpc.ts
··· 1 1 import type { RawSourceMap } from 'vite-node' 2 2 import type { RuntimeRPC } from '../../types' 3 3 import { getEnvironmentTransformMode } from '../../utils/base' 4 - import type { Vitest } from '../core' 4 + import type { WorkspaceProject } from '../workspace' 5 5 6 - export function createMethodsRPC(ctx: Vitest): RuntimeRPC { 6 + export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC { 7 + const ctx = project.ctx 7 8 return { 8 9 async onWorkerExit(error, code) { 9 10 await ctx.logger.printError(error, false, 'Unexpected Exit') ··· 17 18 }, 18 19 async getSourceMap(id, force) { 19 20 if (force) { 20 - const mod = ctx.server.moduleGraph.getModuleById(id) 21 + const mod = project.server.moduleGraph.getModuleById(id) 21 22 if (mod) 22 - ctx.server.moduleGraph.invalidateModule(mod) 23 + project.server.moduleGraph.invalidateModule(mod) 23 24 } 24 - const r = await ctx.vitenode.transformRequest(id) 25 + const r = await project.vitenode.transformRequest(id) 25 26 return r?.map as RawSourceMap | undefined 26 27 }, 27 28 fetch(id, environment) { 28 - const transformMode = getEnvironmentTransformMode(ctx.config, environment) 29 - return ctx.vitenode.fetchModule(id, transformMode) 29 + const transformMode = getEnvironmentTransformMode(project.config, environment) 30 + return project.vitenode.fetchModule(id, transformMode) 30 31 }, 31 32 resolveId(id, importer, environment) { 32 - const transformMode = getEnvironmentTransformMode(ctx.config, environment) 33 - return ctx.vitenode.resolveId(id, importer, transformMode) 33 + const transformMode = getEnvironmentTransformMode(project.config, environment) 34 + return project.vitenode.resolveId(id, importer, transformMode) 34 35 }, 35 36 onPathsCollected(paths) { 36 37 ctx.state.collectPaths(paths) 37 - ctx.report('onPathsCollected', paths) 38 + project.report('onPathsCollected', paths) 38 39 }, 39 40 onCollected(files) { 40 41 ctx.state.collectFiles(files) 41 - ctx.report('onCollected', files) 42 + project.report('onCollected', files) 42 43 }, 43 44 onAfterSuiteRun(meta) { 44 45 ctx.coverageProvider?.onAfterSuiteRun(meta) 45 46 }, 46 47 onTaskUpdate(packs) { 47 48 ctx.state.updateTasks(packs) 48 - ctx.report('onTaskUpdate', packs) 49 + project.report('onTaskUpdate', packs) 49 50 }, 50 51 onUserConsoleLog(log) { 51 52 ctx.state.updateUserLog(log) 52 - ctx.report('onUserConsoleLog', log) 53 + project.report('onUserConsoleLog', log) 53 54 }, 54 55 onUnhandledError(err, type) { 55 56 ctx.state.catchError(err, type) 56 57 }, 57 58 onFinished(files) { 58 - ctx.report('onFinished', files, ctx.state.getUnhandledErrors()) 59 + project.report('onFinished', files, ctx.state.getUnhandledErrors()) 59 60 }, 60 61 } 61 62 }
+55 -34
packages/vitest/src/node/pools/threads.ts
··· 6 6 import type { Options as TinypoolOptions } from 'tinypool' 7 7 import Tinypool from 'tinypool' 8 8 import { distDir } from '../../paths' 9 - import type { ContextTestEnvironment, ResolvedConfig, RuntimeRPC, WorkerContext } from '../../types' 10 - import type { Vitest } from '../core' 9 + import type { ContextTestEnvironment, ResolvedConfig, RuntimeRPC, Vitest, WorkerContext } from '../../types' 11 10 import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' 12 11 import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' 13 12 import { groupBy } from '../../utils/base' 13 + import type { WorkspaceProject } from '../workspace' 14 14 import { createMethodsRPC } from './rpc' 15 15 16 16 const workerPath = pathToFileURL(resolve(distDir, './worker.js')).href 17 17 18 - function createWorkerChannel(ctx: Vitest) { 18 + function createWorkerChannel(project: WorkspaceProject) { 19 19 const channel = new MessageChannel() 20 20 const port = channel.port2 21 21 const workerPort = channel.port1 22 22 23 23 createBirpc<{}, RuntimeRPC>( 24 - createMethodsRPC(ctx), 24 + createMethodsRPC(project), 25 25 { 26 26 post(v) { 27 27 port.postMessage(v) ··· 74 74 const runWithFiles = (name: string): RunWithFiles => { 75 75 let id = 0 76 76 77 - async function runFiles(config: ResolvedConfig, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { 78 - ctx.state.clearFiles(files) 79 - const { workerPort, port } = createWorkerChannel(ctx) 77 + async function runFiles(project: WorkspaceProject, config: ResolvedConfig, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { 78 + ctx.state.clearFiles(project, files) 79 + const { workerPort, port } = createWorkerChannel(project) 80 80 const workerId = ++id 81 81 const data: WorkerContext = { 82 82 port: workerPort, ··· 105 105 const Sequencer = ctx.config.sequence.sequencer 106 106 const sequencer = new Sequencer(ctx) 107 107 108 - return async (files, invalidates) => { 109 - const config = ctx.getSerializableConfig() 108 + return async (specs, invalidates) => { 109 + const configs = new Map<WorkspaceProject, ResolvedConfig>() 110 + const getConfig = (project: WorkspaceProject): ResolvedConfig => { 111 + if (configs.has(project)) 112 + return configs.get(project)! 110 113 111 - if (config.shard) 112 - files = await sequencer.shard(files) 114 + const config = project.getSerializableConfig() 115 + configs.set(project, config) 116 + return config 117 + } 113 118 114 - files = await sequencer.sort(files) 119 + const workspaceMap = new Map<string, WorkspaceProject[]>() 120 + for (const [project, file] of specs) { 121 + const workspaceFiles = workspaceMap.get(file) ?? [] 122 + workspaceFiles.push(project) 123 + workspaceMap.set(file, workspaceFiles) 124 + } 115 125 116 - const filesByEnv = await groupFilesByEnv(files, config) 117 - const envs = envsOrder.concat( 118 - Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), 119 - ) 126 + // it's possible that project defines a file that is also defined by another project 127 + const { shard } = ctx.config 120 128 121 - if (ctx.config.singleThread) { 129 + if (shard) 130 + specs = await sequencer.shard(specs) 131 + 132 + specs = await sequencer.sort(specs) 133 + 134 + const singleThreads = specs.filter(([project]) => project.config.singleThread) 135 + const multipleThreads = specs.filter(([project]) => !project.config.singleThread) 136 + 137 + if (multipleThreads.length) { 138 + const filesByEnv = await groupFilesByEnv(multipleThreads) 139 + const promises = Object.values(filesByEnv).flat() 140 + const results = await Promise.allSettled(promises 141 + .map(({ file, environment, project }) => runFiles(project, getConfig(project), [file], environment, invalidates))) 142 + 143 + const errors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason) 144 + if (errors.length > 0) 145 + throw new AggregateError(errors, 'Errors occurred while running tests. For more information, see serialized error.') 146 + } 147 + 148 + if (singleThreads.length) { 149 + const filesByEnv = await groupFilesByEnv(singleThreads) 150 + const envs = envsOrder.concat( 151 + Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), 152 + ) 153 + 122 154 // always run environments isolated between each other 123 155 for (const env of envs) { 124 156 const files = filesByEnv[env] ··· 126 158 if (!files?.length) 127 159 continue 128 160 129 - const filesByOptions = groupBy(files, ({ environment }) => JSON.stringify(environment.options)) 161 + const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options)) 130 162 131 - for (const option in filesByOptions) { 132 - const files = filesByOptions[option] 163 + const promises = Object.values(filesByOptions).map(async (files) => { 164 + const filenames = files.map(f => f.file) 165 + await runFiles(files[0].project, getConfig(files[0].project), filenames, files[0].environment, invalidates) 166 + }) 133 167 134 - if (files?.length) { 135 - const filenames = files.map(f => f.file) 136 - await runFiles(config, filenames, files[0].environment, invalidates) 137 - } 138 - } 168 + await Promise.all(promises) 139 169 } 140 - } 141 - else { 142 - const promises = Object.values(filesByEnv).flat() 143 - const results = await Promise.allSettled(promises 144 - .map(({ file, environment }) => runFiles(config, [file], environment, invalidates))) 145 - 146 - const errors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason) 147 - if (errors.length > 0) 148 - throw new AggregateError(errors, 'Errors occurred while running tests. For more information, see serialized error.') 149 170 } 150 171 } 151 172 }
+12 -4
packages/vitest/src/node/reporters/base.ts
··· 80 80 if (this.ctx.config.logHeapUsage && task.result.heap != null) 81 81 suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) 82 82 83 - logger.log(` ${getStateSymbol(task)} ${task.name} ${suffix}`) 83 + let title = ` ${getStateSymbol(task)} ` 84 + if (task.projectName) 85 + title += formatProjectName(task.projectName) 86 + title += `${task.name} ${suffix}` 87 + logger.log(title) 84 88 85 89 // print short errors, full errors will be at the end in summary 86 90 for (const test of failed) { ··· 156 160 const BADGE = c.inverse(c.bold(c.blue(' RERUN '))) 157 161 const TRIGGER = trigger ? c.dim(` ${this.relative(trigger)}`) : '' 158 162 const FILENAME_PATTERN = this.ctx.filenamePattern ? `${BADGE_PADDING} ${c.dim('Filename pattern: ')}${c.blue(this.ctx.filenamePattern)}\n` : '' 159 - const TESTNAME_PATTERN = this.ctx.config.testNamePattern ? `${BADGE_PADDING} ${c.dim('Test name pattern: ')}${c.blue(String(this.ctx.config.testNamePattern))}\n` : '' 163 + const TESTNAME_PATTERN = this.ctx.configOverride.testNamePattern ? `${BADGE_PADDING} ${c.dim('Test name pattern: ')}${c.blue(String(this.ctx.configOverride.testNamePattern))}\n` : '' 160 164 161 165 if (files.length > 1) { 162 166 // we need to figure out how to handle rerun all from stdin ··· 214 218 const collectTime = files.reduce((acc, test) => acc + Math.max(0, test.collectDuration || 0), 0) 215 219 const setupTime = files.reduce((acc, test) => acc + Math.max(0, test.setupDuration || 0), 0) 216 220 const testsTime = files.reduce((acc, test) => acc + Math.max(0, test.result?.duration || 0), 0) 217 - const transformTime = Array.from(this.ctx.vitenode.fetchCache.values()).reduce((a, b) => a + (b?.duration || 0), 0) 221 + const transformTime = this.ctx.projects 222 + .flatMap(w => Array.from(w.vitenode.fetchCache.values()).map(i => i.duration || 0)) 223 + .reduce((a, b) => a + b, 0) 224 + const environmentTime = files.reduce((acc, file) => acc + Math.max(0, file.environmentLoad || 0), 0) 225 + const prepareTime = files.reduce((acc, file) => acc + Math.max(0, file.prepareDuration || 0), 0) 218 226 const threadTime = collectTime + testsTime + setupTime 219 227 220 228 const padTitle = (str: string) => c.dim(`${str.padStart(11)} `) ··· 254 262 else if (this.mode === 'typecheck') 255 263 logger.log(padTitle('Duration'), time(executionTime)) 256 264 else 257 - logger.log(padTitle('Duration'), time(executionTime) + c.dim(` (transform ${time(transformTime)}, setup ${time(setupTime)}, collect ${time(collectTime)}, tests ${time(testsTime)})`)) 265 + logger.log(padTitle('Duration'), time(executionTime) + c.dim(` (transform ${time(transformTime)}, setup ${time(setupTime)}, collect ${time(collectTime)}, tests ${time(testsTime)}, environment ${time(environmentTime)}, prepare ${time(prepareTime)})`)) 258 266 259 267 logger.log() 260 268 }
+5 -2
packages/vitest/src/node/reporters/verbose.ts
··· 3 3 import { getFullName } from '../../utils' 4 4 import { F_RIGHT } from '../../utils/figures' 5 5 import { DefaultReporter } from './default' 6 - import { getStateSymbol } from './renderers/utils' 6 + import { formatProjectName, getStateSymbol } from './renderers/utils' 7 7 8 8 export class VerboseReporter extends DefaultReporter { 9 9 constructor() { ··· 17 17 for (const pack of packs) { 18 18 const task = this.ctx.state.idMap.get(pack[0]) 19 19 if (task && task.type === 'test' && task.result?.state && task.result?.state !== 'run') { 20 - let title = ` ${getStateSymbol(task)} ${getFullName(task, c.dim(' > '))}` 20 + let title = ` ${getStateSymbol(task)} ` 21 + if (task.suite?.projectName) 22 + title += formatProjectName(task.suite.projectName) 23 + title += getFullName(task, c.dim(' > ')) 21 24 if (this.ctx.config.logHeapUsage && task.result.heap != null) 22 25 title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) 23 26 this.ctx.logger.log(title)
+15 -11
packages/vitest/src/node/sequencers/BaseSequencer.ts
··· 1 1 import { createHash } from 'node:crypto' 2 - import { resolve } from 'pathe' 2 + import { relative, resolve } from 'pathe' 3 3 import { slash } from 'vite-node/utils' 4 4 import type { Vitest } from '../core' 5 + import type { WorkspaceSpec } from '../pool' 5 6 import type { TestSequencer } from './types' 6 7 7 8 export class BaseSequencer implements TestSequencer { ··· 12 13 } 13 14 14 15 // async so it can be extended by other sequelizers 15 - public async shard(files: string[]): Promise<string[]> { 16 + public async shard(files: WorkspaceSpec[]): Promise<WorkspaceSpec[]> { 16 17 const { config } = this.ctx 17 18 const { index, count } = config.shard! 18 19 const shardSize = Math.ceil(files.length / count) 19 20 const shardStart = shardSize * (index - 1) 20 21 const shardEnd = shardSize * index 21 22 return [...files] 22 - .map((file) => { 23 - const fullPath = resolve(slash(config.root), slash(file)) 23 + .map((spec) => { 24 + const fullPath = resolve(slash(config.root), slash(spec[1])) 24 25 const specPath = fullPath?.slice(config.root.length) 25 26 return { 26 - file, 27 + spec, 27 28 hash: createHash('sha1') 28 29 .update(specPath) 29 30 .digest('hex'), ··· 31 32 }) 32 33 .sort((a, b) => (a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0)) 33 34 .slice(shardStart, shardEnd) 34 - .map(({ file }) => file) 35 + .map(({ spec }) => spec) 35 36 } 36 37 37 38 // async so it can be extended by other sequelizers 38 - public async sort(files: string[]): Promise<string[]> { 39 + public async sort(files: WorkspaceSpec[]): Promise<WorkspaceSpec[]> { 39 40 const cache = this.ctx.cache 40 41 return [...files].sort((a, b) => { 41 - const aState = cache.getFileTestResults(a) 42 - const bState = cache.getFileTestResults(b) 42 + const keyA = `${a[0].getName()}:${relative(this.ctx.config.root, a[1])}` 43 + const keyB = `${b[0].getName()}:${relative(this.ctx.config.root, b[1])}` 44 + 45 + const aState = cache.getFileTestResults(keyA) 46 + const bState = cache.getFileTestResults(keyB) 43 47 44 48 if (!aState || !bState) { 45 - const statsA = cache.getFileStats(a) 46 - const statsB = cache.getFileStats(b) 49 + const statsA = cache.getFileStats(keyA) 50 + const statsB = cache.getFileStats(keyB) 47 51 48 52 // run unknown first 49 53 if (!statsA || !statsB)
+2 -1
packages/vitest/src/node/sequencers/RandomSequencer.ts
··· 1 1 import { shuffle } from '@vitest/utils' 2 + import type { WorkspaceSpec } from '../pool' 2 3 import { BaseSequencer } from './BaseSequencer' 3 4 4 5 export class RandomSequencer extends BaseSequencer { 5 - public async sort(files: string[]) { 6 + public async sort(files: WorkspaceSpec[]) { 6 7 const { sequence } = this.ctx.config 7 8 8 9 return shuffle(files, sequence.seed)
+3 -2
packages/vitest/src/node/sequencers/types.ts
··· 1 1 import type { Awaitable } from '../../types' 2 2 import type { Vitest } from '../core' 3 + import type { WorkspaceSpec } from '../pool' 3 4 4 5 export interface TestSequencer { 5 6 /** 6 7 * Slicing tests into shards. Will be run before `sort`. 7 8 * Only run, if `shard` is defined. 8 9 */ 9 - shard(files: string[]): Awaitable<string[]> 10 - sort(files: string[]): Awaitable<string[]> 10 + shard(files: WorkspaceSpec[]): Awaitable<WorkspaceSpec[]> 11 + sort(files: WorkspaceSpec[]): Awaitable<WorkspaceSpec[]> 11 12 } 12 13 13 14 export interface TestSequencerConstructor {