From 171041af5e4f9e59eb4a446b7561cbfdc9da3c65 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 18 Nov 2024 10:54:09 +0100 Subject: [PATCH] refactor: merge `TestProject` with `WorkspaceProject` (#6906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ari Perkkiƶ --- docs/.vitepress/config.ts | 51 +- docs/advanced/api.md | 215 +++++++- docs/advanced/guide/tests.md | 69 +++ docs/advanced/pool.md | 6 +- docs/advanced/reporters.md | 51 -- packages/browser/src/node/commands/fs.ts | 8 +- packages/browser/src/node/index.ts | 4 +- packages/browser/src/node/plugin.ts | 4 +- packages/browser/src/node/pool.ts | 18 +- .../browser/src/node/providers/playwright.ts | 22 +- .../browser/src/node/providers/preview.ts | 6 +- .../browser/src/node/providers/webdriver.ts | 12 +- packages/browser/src/node/server.ts | 4 +- packages/browser/src/node/utils.ts | 4 +- packages/coverage-v8/src/provider.ts | 4 +- packages/ui/node/reporter.ts | 2 +- packages/vitest/src/api/setup.ts | 2 +- packages/vitest/src/node/cli/cli-api.ts | 2 +- packages/vitest/src/node/core.ts | 104 ++-- packages/vitest/src/node/create.ts | 2 +- packages/vitest/src/node/error.ts | 6 +- packages/vitest/src/node/globalSetup.ts | 9 + packages/vitest/src/node/logger.ts | 16 +- packages/vitest/src/node/plugins/workspace.ts | 6 +- packages/vitest/src/node/pool.ts | 8 +- packages/vitest/src/node/pools/forks.ts | 10 +- packages/vitest/src/node/pools/rpc.ts | 10 +- packages/vitest/src/node/pools/threads.ts | 10 +- packages/vitest/src/node/pools/typecheck.ts | 16 +- packages/vitest/src/node/pools/vmForks.ts | 10 +- packages/vitest/src/node/pools/vmThreads.ts | 10 +- .../src/node/{workspace.ts => project.ts} | 473 ++++++++++++------ .../src/node/reported-workspace-project.ts | 83 --- packages/vitest/src/node/reporters/base.ts | 2 +- packages/vitest/src/node/reporters/blob.ts | 10 +- .../src/node/reporters/github-actions.ts | 6 +- packages/vitest/src/node/reporters/index.ts | 2 +- .../src/node/reporters/reported-tasks.ts | 29 +- packages/vitest/src/node/spec.ts | 13 +- packages/vitest/src/node/state.ts | 14 +- packages/vitest/src/node/types/browser.ts | 6 +- packages/vitest/src/node/types/config.ts | 8 +- .../src/node/workspace/resolveWorkspace.ts | 22 +- packages/vitest/src/public/config.ts | 6 +- packages/vitest/src/public/node.ts | 5 +- packages/vitest/src/typecheck/collect.ts | 4 +- packages/vitest/src/typecheck/typechecker.ts | 4 +- packages/vitest/src/utils/graph.ts | 2 +- packages/vitest/src/utils/test-helpers.ts | 2 +- test/cli/test/reported-tasks.test.ts | 4 +- test/core/test/sequencers.test.ts | 4 +- test/coverage-test/test/threshold-100.test.ts | 2 +- tsconfig.base.json | 1 + 53 files changed, 875 insertions(+), 528 deletions(-) create mode 100644 docs/advanced/guide/tests.md rename packages/vitest/src/node/{workspace.ts => project.ts} (55%) delete mode 100644 packages/vitest/src/node/reported-workspace-project.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 929c93b49c19..532ceb4c467b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -216,24 +216,39 @@ export default ({ mode }: { mode: string }) => { { items: [ { - text: 'Vitest Node API', - link: '/advanced/api', - }, - { - text: 'Runner API', - link: '/advanced/runner', - }, - { - text: 'Task Metadata', - link: '/advanced/metadata', - }, - { - text: 'Extending Reporters', - link: '/advanced/reporters', - }, - { - text: 'Custom Pool', - link: '/advanced/pool', + text: 'API', + items: [ + + { + text: 'Vitest Node API', + link: '/advanced/api', + }, + { + text: 'Runner API', + link: '/advanced/runner', + }, + { + text: 'Task Metadata', + link: '/advanced/metadata', + }, + ], + }, + { + text: 'Guides', + items: [ + { + text: 'Running Tests', + link: '/advanced/guide/tests', + }, + { + text: 'Extending Reporters', + link: '/advanced/reporters', + }, + { + text: 'Custom Pool', + link: '/advanced/pool', + }, + ], }, ], }, diff --git a/docs/advanced/api.md b/docs/advanced/api.md index 4d1fcecaa252..2c3959fb6ade 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -1,3 +1,7 @@ +--- +outline: [2, 3] +--- + # Node API ::: warning @@ -96,7 +100,7 @@ You can start running tests or benchmarks with `start` method. You can pass an a ### `provide` -Vitest exposes `provide` method which is a shorthand for `vitest.getCoreWorkspaceProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. +Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. To recieve the values in the test, you need to import `inject` method from `vitest` entrypont: @@ -123,11 +127,11 @@ declare module 'vitest' { ``` ::: warning -Technically, `provide` is a method of `WorkspaceProject`, so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. +Technically, `provide` is a method of [`TestProject`](#testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. ::: ::: tip -This method is also available to [global setup files](/config/#globalsetup) for cases where you don't want to use the public API: +This method is also available to [global setup files](/config/#globalsetup) for cases where you cannot use the public API: ```js export default function setup({ provide }) { @@ -135,3 +139,208 @@ export default function setup({ provide }) { } ``` ::: + +## TestProject 2.2.0 + +- **Alias**: `WorkspaceProject` before 2.2.0 + +### name + +The name is a unique string assigned by the user or interpreted by Vitest. If user did not provide a name, Vitest tries to load a `package.json` in the root of the project and takes the `name` property from there. If there is no `package.json`, Vitest uses the name of the folder by default. Inline projects use numbers as the name (converted to string). + +::: code-group +```ts [node.js] +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +vitest.projects.map(p => p.name) === [ + '@pkg/server', + 'utils', + '2', + 'custom' +] +``` +```ts [vitest.workspace.js] +export default [ + './packages/server', // has package.json with "@pkg/server" + './utils', // doesn't have a package.json file + { + // doesn't customize the name + test: { + pool: 'threads', + }, + }, + { + // customized the name + test: { + name: 'custom', + }, + }, +] +``` +::: + +### vitest + +`vitest` references the global [`vitest`](#vitest) process. + +### serializedConfig + +This is the test config that all tests will receive. Vitest [serializes config](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/config/serializeConfig.ts) manually by removing all functions and properties that are not possible to serialize. Since this value is available in both tests and node, it is exported from the main entry point. + +```ts +import type { SerializedConfig } from 'vitest' + +const config: SerializedConfig = vitest.projects[0].serializedConfig +``` + +### globalConfig + +The test config that `vitest` was initialized with. If this is the root project, `globalConfig` and `config` will reference the same object. This config is useful for values that cannot be set on the project level, like `coverage` or `reporters`. + +```ts +import type { ResolvedConfig } from 'vitest/node' + +vitest.config === vitest.projects[0].globalConfig +``` + +### config + +This is the project's resolved test config. + +### vite + +This is project's `ViteDevServer`. All projects have their own Vite servers. + +### browser + +This value will be set only if tests are running in the browser. If `browser` is enabled, but tests didn't run yet, this will be `undefined`. If you need to check if the project supports browser tests, use `project.isBrowserSupported()` method. + +::: warning +The browser API is even more experimental and doesn't follow SemVer. The browser API will be standardized separately from the rest of the APIs. +::: + +### provide + +A way to provide custom values to tests in addition to [`config.provide`](/config/#provide) field. All values are validated with [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) before they are stored, but the values on `providedContext` themselves are not cloned. + +::: code-group +```ts [node.js] +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +const project = vitest.projects.find(p => p.name === 'custom') +project.provide('key', 'value') +await vitest.start() +``` +```ts [test.spec.js] +import { inject } from 'vitest' +const value = inject('key') +``` +::: + +The values can be provided dynamicaly. Provided value in tests will be updated on their next run. + +### getProvidedContext + +This returns the context object. Every project also inherits the global context set by `vitest.provide`. + +```ts +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +vitest.provide('global', true) +const project = vitest.projects.find(p => p.name === 'custom') +project.provide('key', 'value') + +// { global: true, key: 'value' } +const context = project.getProvidedContext() +``` + +::: tip +Project context values will always override global ones. +::: + +### createSpecification + +Create a test specification that can be used in `vitest.runFiles`. Specification scopes the test file to a specific `project` and `pool` (optionally). + +```ts +import { createVitest } from 'vitest/node' +import { resolve } from 'node:path/posix' + +const vitest = await createVitest('test') +const project = vitest.projects[0] +const specification = project.createSpecification( + resolve('./basic.test.ts'), + 'threads', // optional override +) +await vitest.runFiles([specification], true) +``` + +::: warning +`createSpecification` expects an absolute file path. It doesn't resolve the file or check that it exists on the file system. +::: + +### isRootProject + +Checks if the current project is the root project. You can also get the root project by calling `vitest.getRootTestProject()`. + +The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. + +The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. + +### globTestFiles + +Globs all test files. This function returns an object with regular tests and typecheck tests: + +```ts +interface GlobReturn { + /** + * Test files that match the filters. + */ + testFiles: string[] + /** + * Typecheck test files that match the filters. This will be empty unless `typecheck.enabled` is `true`. + */ + typecheckTestFiles: string[] +} +``` + +::: tip +Vitest uses [fast-glob](https://www.npmjs.com/package/fast-glob) to find test files. `test.dir`, `test.root`, `root` or `process.cwd()` define the `cwd` option. + +This method looks at several config options: + +- `test.include`, `test.exclude` to find regular test files +- `test.includeSource`, `test.exclude` to find in-source tests +- `test.typecheck.include`, `test.typecheck.exclude` to find typecheck tests +::: + +### matchesTestGlob + +This method checks if the file is a regular test file. It uses the same config properties that `globTestFiles` uses for validation. + +This method also accepts a second parameter, which is the source code. This is used to validate if the file is an in-source test. If you are calling this method several times for several projects it is recommended to read the file once and pass it down directly. + +```ts +import { createVitest } from 'vitest/node' +import { resolve } from 'node:path/posix' + +const vitest = await createVitest('test') +const project = vitest.projects[0] + +project.matchesTestGlob(resolve('./basic.test.ts')) // true +project.matchesTestGlob(resolve('./basic.ts')) // false +project.matchesTestGlob(resolve('./basic.ts'), ` +if (import.meta.vitest) { + // ... +} +`) // true if `includeSource` is set +``` + +### close + +Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. If the resources are needed again, create a new project. + +In detail, this method closes the Vite server, stops the typechecker service, closes the browser if it's running, deletes the temporary directory that holds the source code, and resets the provided context. diff --git a/docs/advanced/guide/tests.md b/docs/advanced/guide/tests.md new file mode 100644 index 000000000000..ce768f7815f3 --- /dev/null +++ b/docs/advanced/guide/tests.md @@ -0,0 +1,69 @@ +# Running Tests + +::: warning +This guide explains how to use the advanced API to run tests via a Node.js script. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. + +Breaking changes might not follow SemVer, please pin Vitest's version when using the experimental API. +::: + +Vitest exposes two methods to initiate Vitest: + +- `startVitest` initiates Vitest, validates the packages are installed and runs tests immidiatly +- `createVitest` only initiates Vitest and doesn't run any tests + +## `startVitest` + +```ts +import { startVitest } from 'vitest/node' + +const vitest = await startVitest( + 'test', + [], // CLI filters + {}, // override test config + {}, // override Vite config + {}, // custom Vitest options +) +const testModules = vitest.state.getTestModules() +for (const testModule of testModules) { + console.log(testModule.moduleId, 'results', testModule.result()) +} +``` + +::: tip +[`TestModule`](/advanced/reporters#TestModule), [`TestSuite`](/advanced/reporters#TestSuite) and [`TestCase`](/advanced/reporters#TestCase) APIs are not experimental and follow SemVer since Vitest 2.1. +::: + +## `createVitest` + +`createVitest` method doesn't validate that required packages are installed. This method also doesn't respect `config.standalone` or `config.mergeReports`. Vitest also won't be closed automatically even if `watch` is disabled. + +```ts +import { createVitest } from 'vitest/node' + +const vitest = await createVitest( + 'test', + {}, // override test config + {}, // override Vite config + {}, // custom Vitest options +) + +// called when `vitest.cancelCurrentRun()` is invoked +vitest.onCancel(() => {}) +// called during `vitest.close()` call +vitest.onClose(() => {}) +// called when Vitest reruns test files +vitest.onTestsRerun((files) => {}) + +try { + // this will set process.exitCode to 1 if tests failed + await vitest.start(['my-filter']) +} +catch (err) { + // this can throw + // "FilesNotFoundError" if no files were found + // "GitNotFoundError" if `--changed` is enabled and repository is not initialized +} +finally { + await vitest.close() +} +``` diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 2f7368dbfcd9..f9f52889298e 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -40,12 +40,12 @@ export default defineConfig({ The file specified in `pool` option should export a function (can be async) that accepts `Vitest` interface as its first option. This function needs to return an object matching `ProcessPool` interface: ```ts -import { ProcessPool, WorkspaceProject } from 'vitest/node' +import { ProcessPool, TestSpecification } from 'vitest/node' export interface ProcessPool { name: string - runTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise - collectTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise + runTests: (files: TestSpecification[], invalidates?: string[]) => Promise + collectTests: (files: TestSpecification[], invalidates?: string[]) => Promise close?: () => Promise } ``` diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index d9dd0e1da6a0..6f3236632cd0 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -87,8 +87,6 @@ class MyReporter implements Reporter { } } ``` - -We are planning to stabilize this API in Vitest 2.1. ::: ### TestCase @@ -384,55 +382,6 @@ function onFileCollected(testModule: TestModule): void { } ``` -### TestProject - -`TestProject` is a project assosiated with the module. Every test and suite inside that module will reference the same project. - -Project is useful to get the configuration or provided context. - -```ts -declare class TestProject { - /** - * The global vitest instance. - * @experimental The public Vitest API is experimental and does not follow semver. - */ - readonly vitest: Vitest - /** - * The workspace project this test project is associated with. - * @experimental The public Vitest API is experimental and does not follow semver. - */ - readonly workspaceProject: WorkspaceProject - /** - * Vite's dev server instance. Every workspace project has its own server. - */ - readonly vite: ViteDevServer - /** - * Resolved project configuration. - */ - readonly config: ResolvedProjectConfig - /** - * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. - */ - readonly globalConfig: ResolvedConfig - /** - * Serialized project configuration. This is the config that tests receive. - */ - get serializedConfig(): SerializedConfig - /** - * The name of the project or an empty string if not set. - */ - name(): string - /** - * Custom context provided to the project. - */ - context(): ProvidedContext - /** - * Provide a custom serializable context to the project. This context will be available for tests once they run. - */ - provide(key: T, value: ProvidedContext[T]): void -} -``` - ## Exported Reporters `vitest` comes with a few [built-in reporters](/guide/reporters) that you can use out of the box. diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index 7dc30924250a..fba82a384c86 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -1,14 +1,14 @@ -import type { BrowserCommand, WorkspaceProject } from 'vitest/node' +import type { BrowserCommand, TestProject } from 'vitest/node' import type { BrowserCommands } from '../../../context' import fs, { promises as fsp } from 'node:fs' import { basename, dirname, resolve } from 'node:path' import mime from 'mime/lite' import { isFileServingAllowed } from 'vitest/node' -function assertFileAccess(path: string, project: WorkspaceProject) { +function assertFileAccess(path: string, project: TestProject) { if ( - !isFileServingAllowed(path, project.server) - && !isFileServingAllowed(path, project.ctx.server) + !isFileServingAllowed(path, project.vite) + && !isFileServingAllowed(path, project.vitest.server) ) { throw new Error( `Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`, diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 7d91208dee5b..c7af212b3c98 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -1,5 +1,5 @@ import type { Plugin } from 'vitest/config' -import type { WorkspaceProject } from 'vitest/node' +import type { TestProject } from 'vitest/node' import c from 'tinyrainbow' import { createViteLogger, createViteServer } from 'vitest/node' import { version } from '../../package.json' @@ -13,7 +13,7 @@ export { createBrowserPool } from './pool' export type { BrowserServer } from './server' export async function createBrowserServer( - project: WorkspaceProject, + project: TestProject, configFile: string | undefined, prePlugins: Plugin[] = [], postPlugins: Plugin[] = [], diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index e1d049d7c702..f52a5b0b0015 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -1,6 +1,6 @@ import type { Stats } from 'node:fs' import type { HtmlTagDescriptor } from 'vite' -import type { WorkspaceProject } from 'vitest/node' +import type { TestProject } from 'vitest/node' import type { BrowserServer } from './server' import { lstatSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' @@ -583,7 +583,7 @@ function getRequire() { return _require } -function resolveCoverageFolder(project: WorkspaceProject) { +function resolveCoverageFolder(project: TestProject) { const options = project.ctx.config const htmlReporter = options.coverage?.enabled ? toArray(options.coverage.reporter).find((reporter) => { diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index 9079535bee75..42c8a9654710 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -1,4 +1,4 @@ -import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject, WorkspaceSpec } from 'vitest/node' +import type { BrowserProvider, ProcessPool, TestProject, TestSpecification, Vitest } from 'vitest/node' import crypto from 'node:crypto' import * as nodeos from 'node:os' import { relative } from 'pathe' @@ -9,7 +9,7 @@ const debug = createDebugger('vitest:browser:pool') async function waitForTests( method: 'run' | 'collect', contextId: string, - project: WorkspaceProject, + project: TestProject, files: string[], ) { const context = project.browser!.state.createAsyncContext(method, contextId, files) @@ -19,7 +19,7 @@ async function waitForTests( export function createBrowserPool(ctx: Vitest): ProcessPool { const providers = new Set() - const executeTests = async (method: 'run' | 'collect', project: WorkspaceProject, files: string[]) => { + const executeTests = async (method: 'run' | 'collect', project: TestProject, files: string[]) => { ctx.state.clearFiles(project, files) const browser = project.browser! @@ -113,11 +113,11 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { await Promise.all(promises) } - const runWorkspaceTests = async (method: 'run' | 'collect', specs: WorkspaceSpec[]) => { - const groupedFiles = new Map() - for (const [project, file] of specs) { + const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => { + const groupedFiles = new Map() + for (const { project, moduleId } of specs) { const files = groupedFiles.get(project) || [] - files.push(file) + files.push(moduleId) groupedFiles.set(project, files) } @@ -131,7 +131,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { if (isCancelled) { break } - await project.initBrowserProvider() + await project._initBrowserProvider() await executeTests(method, project, files) } @@ -142,7 +142,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { ? nodeos.availableParallelism() : nodeos.cpus().length - function getThreadsCount(project: WorkspaceProject) { + function getThreadsCount(project: TestProject) { const config = project.config.browser if (!config.headless || !project.browser!.provider.supportsParallelism) { return 1 diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index cb24179e8479..fa9bf02b678f 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -9,7 +9,7 @@ import type { import type { BrowserProvider, BrowserProviderInitializationOptions, - WorkspaceProject, + TestProject, } from 'vitest/node' export const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const @@ -27,7 +27,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { public browser: Browser | null = null private browserName!: PlaywrightBrowser - private ctx!: WorkspaceProject + private project!: TestProject private options?: { launch?: LaunchOptions @@ -44,10 +44,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } initialize( - project: WorkspaceProject, + project: TestProject, { browser, options }: PlaywrightProviderOptions, ) { - this.ctx = project + this.project = project this.browserName = browser this.options = options as any } @@ -62,7 +62,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } this.browserPromise = (async () => { - const options = this.ctx.config.browser + const options = this.project.config.browser const playwright = await import('playwright') @@ -71,20 +71,20 @@ export class PlaywrightBrowserProvider implements BrowserProvider { headless: options.headless, } satisfies LaunchOptions - if (this.ctx.config.inspector.enabled) { + if (this.project.config.inspector.enabled) { // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector - const port = this.ctx.config.inspector.port || 9229 - const host = this.ctx.config.inspector.host || '127.0.0.1' + const port = this.project.config.inspector.port || 9229 + const host = this.project.config.inspector.host || '127.0.0.1' launchOptions.args ||= [] launchOptions.args.push(`--remote-debugging-port=${port}`) launchOptions.args.push(`--remote-debugging-address=${host}`) - this.ctx.logger.log(`Debugger listening on ws://${host}:${port}`) + this.project.logger.log(`Debugger listening on ws://${host}:${port}`) } // start Vitest UI maximized only on supported browsers - if (this.ctx.config.browser.ui && this.browserName === 'chromium') { + if (this.project.config.browser.ui && this.browserName === 'chromium') { if (!launchOptions.args) { launchOptions.args = [] } @@ -113,7 +113,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { ignoreHTTPSErrors: true, serviceWorkers: 'allow', } satisfies BrowserContextOptions - if (this.ctx.config.browser.ui) { + if (this.project.config.browser.ui) { options.viewport = null } const context = await browser.newContext(options) diff --git a/packages/browser/src/node/providers/preview.ts b/packages/browser/src/node/providers/preview.ts index a8e8829a7cfd..2a495996d469 100644 --- a/packages/browser/src/node/providers/preview.ts +++ b/packages/browser/src/node/providers/preview.ts @@ -1,9 +1,9 @@ -import type { BrowserProvider, WorkspaceProject } from 'vitest/node' +import type { BrowserProvider, TestProject } from 'vitest/node' export class PreviewBrowserProvider implements BrowserProvider { public name = 'preview' as const public supportsParallelism: boolean = false - private project!: WorkspaceProject + private project!: TestProject private open = false getSupportedBrowsers() { @@ -19,7 +19,7 @@ export class PreviewBrowserProvider implements BrowserProvider { return {} } - async initialize(project: WorkspaceProject) { + async initialize(project: TestProject) { this.project = project this.open = false if (project.config.browser.headless) { diff --git a/packages/browser/src/node/providers/webdriver.ts b/packages/browser/src/node/providers/webdriver.ts index 810d91ab1916..c28584f42b1b 100644 --- a/packages/browser/src/node/providers/webdriver.ts +++ b/packages/browser/src/node/providers/webdriver.ts @@ -1,7 +1,7 @@ import type { BrowserProvider, BrowserProviderInitializationOptions, - WorkspaceProject, + TestProject, } from 'vitest/node' import type { RemoteOptions } from 'webdriverio' @@ -20,7 +20,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { public browser: WebdriverIO.Browser | null = null private browserName!: WebdriverBrowser - private ctx!: WorkspaceProject + private project!: TestProject private options?: RemoteOptions @@ -29,10 +29,10 @@ export class WebdriverBrowserProvider implements BrowserProvider { } async initialize( - ctx: WorkspaceProject, + ctx: TestProject, { browser, options }: WebdriverProviderOptions, ) { - this.ctx = ctx + this.project = ctx this.browserName = browser this.options = options as RemoteOptions } @@ -61,7 +61,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { return this.browser } - const options = this.ctx.config.browser + const options = this.project.config.browser if (this.browserName === 'safari') { if (options.headless) { @@ -95,7 +95,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { edge: ['ms:edgeOptions', ['--headless']], } as const - const options = this.ctx.config.browser + const options = this.project.config.browser const browser = this.browserName if (browser !== 'safari' && options.headless) { const [key, args] = headlessMap[browser] diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index a07991d95647..f50aaf34892d 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -5,8 +5,8 @@ import type { BrowserScript, CDPSession, BrowserServer as IBrowserServer, + TestProject, Vite, - WorkspaceProject, } from 'vitest/node' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' @@ -42,7 +42,7 @@ export class BrowserServer implements IBrowserServer { private stackTraceOptions: StackTraceParserOptions constructor( - public project: WorkspaceProject, + public project: TestProject, public base: string, ) { this.stackTraceOptions = { diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts index b1b3c206b7f1..3d3e92ad37c5 100644 --- a/packages/browser/src/node/utils.ts +++ b/packages/browser/src/node/utils.ts @@ -1,4 +1,4 @@ -import type { BrowserProviderModule, ResolvedBrowserOptions, WorkspaceProject } from 'vitest/node' +import type { BrowserProviderModule, ResolvedBrowserOptions, TestProject } from 'vitest/node' export function replacer(code: string, values: Record) { return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _) @@ -8,7 +8,7 @@ const builtinProviders = ['webdriverio', 'playwright', 'preview'] export async function getBrowserProvider( options: ResolvedBrowserOptions, - project: WorkspaceProject, + project: TestProject, ): Promise { if (options.provider == null || builtinProviders.includes(options.provider)) { const providers = await import('./providers') diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 340d9457ccef..f5942989a7d4 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -2,7 +2,7 @@ import type { CoverageMap } from 'istanbul-lib-coverage' import type { Profiler } from 'node:inspector' import type { EncodedSourceMap, FetchResult } from 'vite-node' import type { AfterSuiteRunMeta } from 'vitest' -import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest, WorkspaceProject } from 'vitest/node' +import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vitest } from 'vitest/node' import { promises as fs } from 'node:fs' import { fileURLToPath, pathToFileURL } from 'node:url' import remapping from '@ampproject/remapping' @@ -288,7 +288,7 @@ export class V8CoverageProvider extends BaseCoverageProvider { let fetchCache = project.vitenode.fetchCache diff --git a/packages/ui/node/reporter.ts b/packages/ui/node/reporter.ts index 002ea60751ce..696c13e7e0b4 100644 --- a/packages/ui/node/reporter.ts +++ b/packages/ui/node/reporter.ts @@ -63,7 +63,7 @@ export default class HTMLReporter implements Reporter { const result: HTMLReportData = { paths: this.ctx.state.getPaths(), files: this.ctx.state.getFiles(), - config: this.ctx.getCoreWorkspaceProject().getSerializableConfig(), + config: this.ctx.getRootTestProject().serializedConfig, unhandledErrors: this.ctx.state.getUnhandledErrors(), moduleGraph: {}, sources: {}, diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index f0554d62e2b4..6d4621a7696c 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -76,7 +76,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { await ctx.rerunFiles(files) }, getConfig() { - return ctx.getCoreWorkspaceProject().getSerializableConfig() + return ctx.getRootTestProject().serializedConfig }, async getTransformResult(projectName: string, id, browser = false) { const project = ctx.getProjectByName(projectName) diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 57ce175cc310..e36d07432768 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -42,7 +42,7 @@ export async function startVitest( options: CliOptions = {}, viteOverrides?: ViteUserConfig, vitestOptions?: VitestOptions, -): Promise { +): Promise { const root = resolve(options.root || process.cwd()) const ctx = await prepareVitest( diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index f10eb087b8a0..712690acd626 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -9,7 +9,7 @@ import type { TestSpecification } from './spec' import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' -import { existsSync, promises as fs } from 'node:fs' +import { existsSync, promises as fs, readFileSync } from 'node:fs' import { getTasks, hasFailed } from '@vitest/runner/utils' import { SnapshotManager } from '@vitest/snapshot/manager' import { noop, slash, toArray } from '@vitest/utils' @@ -28,11 +28,11 @@ import { resolveConfig } from './config/resolveConfig' import { FilesNotFoundError, GitNotFoundError } from './errors' import { Logger } from './logger' import { VitestPackageInstaller } from './packageInstaller' -import { createPool, getFilePoolName } from './pool' +import { createPool } from './pool' +import { TestProject } from './project' import { BlobReporter, readBlobs } from './reporters/blob' import { createBenchmarkReporters, createReporters } from './reporters/utils' import { StateManager } from './state' -import { WorkspaceProject } from './workspace' import { resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -75,15 +75,16 @@ export class Vitest { public packageInstaller: VitestPackageInstaller - private coreWorkspaceProject!: WorkspaceProject + /** @internal */ + public coreWorkspaceProject!: TestProject /** @private */ - public resolvedProjects: WorkspaceProject[] = [] - public projects: WorkspaceProject[] = [] + public resolvedProjects: TestProject[] = [] + public projects: TestProject[] = [] public distPath = distDir - private _cachedSpecs = new Map() + private _cachedSpecs = new Map() private _workspaceConfigPath?: string /** @deprecated use `_cachedSpecs` */ @@ -193,7 +194,7 @@ export class Vitest { ) } if (!this.coreWorkspaceProject) { - this.coreWorkspaceProject = WorkspaceProject.createBasicProject(this) + this.coreWorkspaceProject = TestProject._createBasicProject(this) } if (this.config.testNamePattern) { @@ -204,42 +205,38 @@ export class Vitest { } public provide(key: T, value: ProvidedContext[T]) { - this.getCoreWorkspaceProject().provide(key, value) - } - - /** - * @deprecated internal, use `_createCoreProject` instead - */ - createCoreProject() { - return this._createCoreProject() + this.getRootTestProject().provide(key, value) } /** * @internal */ - async _createCoreProject() { - this.coreWorkspaceProject = await WorkspaceProject.createCoreProject(this) + _createCoreProject() { + this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } - public getCoreWorkspaceProject(): WorkspaceProject { + public getRootTestProject(): TestProject { + if (!this.coreWorkspaceProject) { + throw new Error(`Root project is not initialized. This means that the Vite server was not established yet and the the workspace config is not resolved.`) + } return this.coreWorkspaceProject } /** * @deprecated use Reported Task API instead */ - public getProjectByTaskId(taskId: string): WorkspaceProject { + public getProjectByTaskId(taskId: string): TestProject { const task = this.state.idMap.get(taskId) const projectName = (task as File).projectName || task?.file?.projectName || '' - return this.projects.find(p => p.getName() === projectName) - || this.getCoreWorkspaceProject() + return this.projects.find(p => p.name === projectName) + || this.getRootTestProject() || this.projects[0] } public getProjectByName(name: string = '') { - return this.projects.find(p => p.getName() === name) - || this.getCoreWorkspaceProject() + return this.projects.find(p => p.name === name) + || this.coreWorkspaceProject || this.projects[0] } @@ -271,7 +268,7 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return [await this._createCoreProject()] + return [this._createCoreProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { @@ -315,7 +312,7 @@ export class Vitest { await this.report('onInit', this) await this.report('onPathsCollected', files.flatMap(f => f.filepath)) - const workspaceSpecs = new Map() + const workspaceSpecs = new Map() for (const file of files) { const project = this.getProjectByName(file.projectName) const specs = workspaceSpecs.get(project) || [] @@ -454,20 +451,20 @@ export class Vitest { } private async getTestDependencies(spec: WorkspaceSpec, deps = new Set()) { - const addImports = async (project: WorkspaceProject, filepath: string) => { + const addImports = async (project: TestProject, filepath: string) => { if (deps.has(filepath)) { return } deps.add(filepath) - const mod = project.server.moduleGraph.getModuleById(filepath) + const mod = project.vite.moduleGraph.getModuleById(filepath) const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) if (!transformed) { return } const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] await Promise.all(dependencies.map(async (dep) => { - const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true }) + const path = await project.vite.pluginContainer.resolveId(dep, filepath, { ssr: true }) const fsPath = path && !path.external && path.id.split('?')[0] if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { await addImports(project, fsPath) @@ -475,7 +472,7 @@ export class Vitest { })) } - await addImports(spec.project.workspaceProject, spec.moduleId) + await addImports(spec.project, spec.moduleId) deps.delete(spec.moduleId) return deps @@ -534,7 +531,7 @@ export class Vitest { * @deprecated remove when vscode extension supports "getFileWorkspaceSpecs" */ getProjectsByTestFile(file: string) { - return this.getFileWorkspaceSpecs(file) + return this.getFileWorkspaceSpecs(file) as WorkspaceSpec[] } getFileWorkspaceSpecs(file: string) { @@ -543,14 +540,13 @@ export class Vitest { return _cached } - const specs: WorkspaceSpec[] = [] + const specs: TestSpecification[] = [] for (const project of this.projects) { if (project.isTestFile(file)) { - const pool = getFilePoolName(project, file) - specs.push(project.createSpec(file, pool)) + specs.push(project.createSpecification(file)) } if (project.isTypecheckFile(file)) { - specs.push(project.createSpec(file, 'typescript')) + specs.push(project.createSpecification(file, 'typescript')) } } specs.forEach(spec => this.ensureSpecCached(spec)) @@ -558,13 +554,13 @@ export class Vitest { } async initializeGlobalSetup(paths: TestSpecification[]) { - const projects = new Set(paths.map(spec => spec.project.workspaceProject)) - const coreProject = this.getCoreWorkspaceProject() + const projects = new Set(paths.map(spec => spec.project)) + const coreProject = this.getRootTestProject() if (!projects.has(coreProject)) { projects.add(coreProject) } for (const project of projects) { - await project.initializeGlobalSetup() + await project._initializeGlobalSetup() } } @@ -688,7 +684,7 @@ export class Vitest { } async initBrowserServers() { - await Promise.all(this.projects.map(p => p.initBrowserServer())) + await Promise.all(this.projects.map(p => p._initBrowserServer())) } async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true) { @@ -889,14 +885,15 @@ export class Vitest { onAdd = async (id: string) => { id = slash(id) this.updateLastChanged(id) + const fileContent = readFileSync(id, 'utf-8') - const matchingProjects: WorkspaceProject[] = [] - await Promise.all(this.projects.map(async (project) => { - if (await project.isTargetFile(id)) { + const matchingProjects: TestProject[] = [] + this.projects.forEach((project) => { + if (project.matchesTestGlob(id, fileContent)) { matchingProjects.push(project) - project.testFilesList?.push(id) + project._markTestFile(id) } - })) + }) if (matchingProjects.length > 0) { this.changedTests.add(id) @@ -1028,11 +1025,11 @@ export class Vitest { teardownProjects.push(this.coreWorkspaceProject) } // do teardown before closing the server - for await (const project of teardownProjects.reverse()) { - await project.teardownGlobalSetup() + for (const project of teardownProjects.reverse()) { + await project._teardownGlobalSetup() } - const closePromises: unknown[] = this.resolvedProjects.map(w => w.close().then(() => w.server = undefined as any)) + const closePromises: unknown[] = this.resolvedProjects.map(w => w.close()) // close the core workspace server only once // it's possible that it's not initialized at all because it's not running any tests if (!this.resolvedProjects.includes(this.coreWorkspaceProject)) { @@ -1107,32 +1104,31 @@ export class Vitest { } public async globTestSpecs(filters: string[] = []) { - const files: WorkspaceSpec[] = [] + const files: TestSpecification[] = [] await Promise.all(this.projects.map(async (project) => { const { testFiles, typecheckTestFiles } = await project.globTestFiles(filters) testFiles.forEach((file) => { - const pool = getFilePoolName(project, file) - const spec = project.createSpec(file, pool) + const spec = project.createSpecification(file) this.ensureSpecCached(spec) files.push(spec) }) typecheckTestFiles.forEach((file) => { - const spec = project.createSpec(file, 'typescript') + const spec = project.createSpecification(file, 'typescript') this.ensureSpecCached(spec) files.push(spec) }) })) - return files + return files as WorkspaceSpec[] } /** - * @deprecated use globTestSpecs instead + * @deprecated use `globTestSpecs` instead */ public async globTestFiles(filters: string[] = []) { return this.globTestSpecs(filters) } - private ensureSpecCached(spec: WorkspaceSpec) { + private ensureSpecCached(spec: TestSpecification) { const file = spec[1] const specs = this._cachedSpecs.get(file) || [] const included = specs.some(_s => _s[0] === spec[0] && _s[2].pool === spec[2].pool) diff --git a/packages/vitest/src/node/create.ts b/packages/vitest/src/node/create.ts index cf64cfaedd52..8d0fe76a8949 100644 --- a/packages/vitest/src/node/create.ts +++ b/packages/vitest/src/node/create.ts @@ -18,7 +18,7 @@ export async function createVitest( options: UserConfig, viteOverrides: ViteUserConfig = {}, vitestOptions: VitestOptions = {}, -) { +): Promise { const ctx = new Vitest(mode, vitestOptions) const root = slash(resolve(options.root || process.cwd())) diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts index 54f928df8d43..44de1ea400db 100644 --- a/packages/vitest/src/node/error.ts +++ b/packages/vitest/src/node/error.ts @@ -1,7 +1,7 @@ import type { ErrorWithDiff, ParsedStack } from '@vitest/utils' import type { Vitest } from './core' import type { ErrorOptions } from './logger' -import type { WorkspaceProject } from './workspace' +import type { TestProject } from './project' /* eslint-disable prefer-template */ import { existsSync, readFileSync } from 'node:fs' import { Writable } from 'node:stream' @@ -55,7 +55,7 @@ export function capturePrintError( export function printError( error: unknown, - project: WorkspaceProject | undefined, + project: TestProject | undefined, options: PrintErrorOptions, ): PrintErrorResult | undefined { const { showCodeFrame = true, type, printProperties = true } = options @@ -338,7 +338,7 @@ function printErrorMessage(error: ErrorWithDiff, logger: Logger) { export function printStack( logger: Logger, - project: WorkspaceProject, + project: TestProject, stack: ParsedStack[], highlight: ParsedStack | undefined, errorProperties: Record, diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts index 33d17d8aadfe..295b8a862035 100644 --- a/packages/vitest/src/node/globalSetup.ts +++ b/packages/vitest/src/node/globalSetup.ts @@ -4,11 +4,20 @@ import type { ResolvedConfig } from './types/config' import { toArray } from '@vitest/utils' export interface GlobalSetupContext { + /** + * Config of the current project. + */ config: ResolvedConfig + /** + * Provide a value to the test context. This value will be available to all tests via `inject`. + */ provide: ( key: T, value: ProvidedContext[T] ) => void + /** + * Register a function that will be called before tests run again in watch mode. + */ onTestsRerun: (cb: OnTestsRerunHandler) => void } diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index 5254bf6b3d4e..fd3ec64292e9 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -4,7 +4,7 @@ import type { Writable } from 'node:stream' import type { TypeCheckError } from '../typecheck/typechecker' import type { Vitest } from './core' import type { PrintErrorResult } from './error' -import type { WorkspaceProject } from './workspace' +import type { TestProject } from './project' import { Console } from 'node:console' import { toArray } from '@vitest/utils' import { parseErrorStacktrace } from '@vitest/utils/source-map' @@ -18,7 +18,7 @@ import { RandomSequencer } from './sequencers/RandomSequencer' export interface ErrorOptions { type?: string fullStack?: boolean - project?: WorkspaceProject + project?: TestProject verbose?: boolean screenshotPaths?: string[] task?: Task @@ -105,7 +105,7 @@ export class Logger { printError(err: unknown, options: ErrorOptions = {}): PrintErrorResult | undefined { const { fullStack = false, type } = options const project = options.project - ?? this.ctx.getCoreWorkspaceProject() + ?? this.ctx.coreWorkspaceProject ?? this.ctx.projects[0] return printError(err, project, { type, @@ -163,8 +163,7 @@ export class Logger { } this.ctx.projects.forEach((project) => { const config = project.config - const name = project.getName() - const output = project.isCore() || !name ? '' : `[${name}]` + const output = (project.isRootProject() || !project.name) ? '' : `[${project.name}]` if (output) { this.console.error(c.bgCyan(`${output} Config`)) } @@ -245,7 +244,7 @@ export class Logger { } } - printBrowserBanner(project: WorkspaceProject) { + printBrowserBanner(project: TestProject) { if (!project.browser) { return } @@ -256,10 +255,9 @@ export class Logger { return } - const name = project.getName() - const output = project.isCore() + const output = project.isRootProject() ? '' - : formatProjectName(name) + : formatProjectName(project.name) const provider = project.browser.provider.name const providerString = provider === 'preview' ? '' : ` by ${c.reset(c.bold(provider))}` this.log( diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 8f4e3c9027e0..f9c066cc1020 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -1,6 +1,6 @@ import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' +import type { TestProject } from '../project' import type { ResolvedConfig, UserWorkspaceConfig } from '../types/config' -import type { WorkspaceProject } from '../workspace' import { existsSync, readFileSync } from 'node:fs' import { deepMerge } from '@vitest/utils' import { basename, dirname, relative, resolve } from 'pathe' @@ -26,7 +26,7 @@ interface WorkspaceOptions extends UserWorkspaceConfig { } export function WorkspaceVitestPlugin( - project: WorkspaceProject, + project: TestProject, options: WorkspaceOptions, ) { return [ @@ -153,7 +153,7 @@ export function WorkspaceVitestPlugin( }, async configureServer(server) { const options = deepMerge({}, configDefaults, server.config.test || {}) - await project.setServer(options, server) + await project._configureServer(options, server) await server.watcher.close() }, diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index eef580e696d5..ef11f6167966 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -1,8 +1,8 @@ import type { Awaitable } from '@vitest/utils' import type { Vitest } from './core' +import type { TestProject } from './project' import type { TestSpecification } from './spec' import type { BuiltinPool, Pool } from './types/pool-options' -import type { WorkspaceProject } from './workspace' import mm from 'micromatch' import { isWindows } from '../utils/env' import { createForksPool } from './pools/forks' @@ -18,7 +18,7 @@ export type WorkspaceSpec = TestSpecification & [ /** * @deprecated use spec.project instead */ - project: WorkspaceProject, + project: TestProject, /** * @deprecated use spec.moduleId instead */ @@ -57,14 +57,14 @@ export const builtinPools: BuiltinPool[] = [ 'typescript', ] -function getDefaultPoolName(project: WorkspaceProject): Pool { +function getDefaultPoolName(project: TestProject): Pool { if (project.config.browser.enabled) { return 'browser' } return project.config.pool } -export function getFilePoolName(project: WorkspaceProject, file: string) { +export function getFilePoolName(project: TestProject, file: string) { for (const [glob, pool] of project.config.poolMatchGlobs) { if ((pool as Pool) === 'browser') { throw new Error( diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index cf925eb421fb..4b4c015c4eb4 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -3,8 +3,8 @@ import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' import type { Vitest } from '../core' import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' +import type { TestProject } from '../project' import type { SerializedConfig } from '../types/config' -import type { WorkspaceProject } from '../workspace' import EventEmitter from 'node:events' import * as nodeos from 'node:os' import { resolve } from 'node:path' @@ -16,7 +16,7 @@ import { wrapSerializableConfig } from '../../utils/config-helpers' import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' import { createMethodsRPC } from './rpc' -function createChildProcessChannel(project: WorkspaceProject) { +function createChildProcessChannel(project: TestProject) { const emitter = new EventEmitter() const cleanup = () => emitter.removeAllListeners() @@ -99,7 +99,7 @@ export function createForksPool( let id = 0 async function runFiles( - project: WorkspaceProject, + project: TestProject, config: SerializedConfig, files: string[], environment: ContextTestEnvironment, @@ -153,8 +153,8 @@ export function createForksPool( // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) - const configs = new WeakMap() - const getConfig = (project: WorkspaceProject): SerializedConfig => { + const configs = new WeakMap() + const getConfig = (project: TestProject): SerializedConfig => { if (configs.has(project)) { return configs.get(project)! } diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 28b65a4bcd97..4c41417fdae8 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -1,7 +1,7 @@ import type { RawSourceMap } from 'vite-node' import type { RuntimeRPC } from '../../types/rpc' +import type { TestProject } from '../project' import type { ResolveSnapshotPathHandlerContext } from '../types/config' -import type { WorkspaceProject } from '../workspace' import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'pathe' import { hash } from '../hash' @@ -13,7 +13,7 @@ interface MethodsOptions { cacheFs?: boolean } -export function createMethodsRPC(project: WorkspaceProject, options: MethodsOptions = {}): RuntimeRPC { +export function createMethodsRPC(project: TestProject, options: MethodsOptions = {}): RuntimeRPC { const ctx = project.ctx const cacheFs = options.cacheFs ?? false return { @@ -22,14 +22,14 @@ export function createMethodsRPC(project: WorkspaceProject, options: MethodsOpti }, resolveSnapshotPath(testPath: string) { return ctx.snapshot.resolvePath(testPath, { - config: project.getSerializableConfig(), + config: project.serializedConfig, }) }, async getSourceMap(id, force) { if (force) { - const mod = project.server.moduleGraph.getModuleById(id) + const mod = project.vite.moduleGraph.getModuleById(id) if (mod) { - project.server.moduleGraph.invalidateModule(mod) + project.vite.moduleGraph.invalidateModule(mod) } } const r = await project.vitenode.transformRequest(id) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 4d443975057a..bfb224a6560a 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -3,9 +3,9 @@ import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' import type { Vitest } from '../core' import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' +import type { TestProject } from '../project' import type { SerializedConfig } from '../types/config' import type { WorkerContext } from '../types/worker' -import type { WorkspaceProject } from '../workspace' import * as nodeos from 'node:os' import { resolve } from 'node:path' import { MessageChannel } from 'node:worker_threads' @@ -15,7 +15,7 @@ import { groupBy } from '../../utils/base' import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' import { createMethodsRPC } from './rpc' -function createWorkerChannel(project: WorkspaceProject) { +function createWorkerChannel(project: TestProject) { const channel = new MessageChannel() const port = channel.port2 const workerPort = channel.port1 @@ -93,7 +93,7 @@ export function createThreadsPool( let id = 0 async function runFiles( - project: WorkspaceProject, + project: TestProject, config: SerializedConfig, files: string[], environment: ContextTestEnvironment, @@ -151,8 +151,8 @@ export function createThreadsPool( // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) - const configs = new WeakMap() - const getConfig = (project: WorkspaceProject): SerializedConfig => { + const configs = new WeakMap() + const getConfig = (project: TestProject): SerializedConfig => { if (configs.has(project)) { return configs.get(project)! } diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index 805e28b92695..5a0bcae3a9c3 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -2,18 +2,18 @@ import type { DeferPromise } from '@vitest/utils' import type { TypecheckResults } from '../../typecheck/typechecker' import type { Vitest } from '../core' import type { ProcessPool, WorkspaceSpec } from '../pool' -import type { WorkspaceProject } from '../workspace' +import type { TestProject } from '../project' import { hasFailed } from '@vitest/runner/utils' import { createDefer } from '@vitest/utils' import { Typechecker } from '../../typecheck/typechecker' import { groupBy } from '../../utils/base' export function createTypecheckPool(ctx: Vitest): ProcessPool { - const promisesMap = new WeakMap>() - const rerunTriggered = new WeakSet() + const promisesMap = new WeakMap>() + const rerunTriggered = new WeakSet() async function onParseEnd( - project: WorkspaceProject, + project: TestProject, { files, sourceErrors }: TypecheckResults, ) { const checker = project.typechecker! @@ -49,7 +49,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { } async function createWorkspaceTypechecker( - project: WorkspaceProject, + project: TestProject, files: string[], ) { const checker = project.typechecker ?? new Typechecker(project) @@ -90,7 +90,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { return checker } - async function startTypechecker(project: WorkspaceProject, files: string[]) { + async function startTypechecker(project: TestProject, files: string[]) { if (project.typechecker) { return project.typechecker } @@ -104,10 +104,10 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { for (const name in specsByProject) { const project = specsByProject[name][0].project const files = specsByProject[name].map(spec => spec.moduleId) - const checker = await createWorkspaceTypechecker(project.workspaceProject, files) + const checker = await createWorkspaceTypechecker(project, files) checker.setFiles(files) await checker.collectTests() - ctx.state.collectFiles(project.workspaceProject, checker.getTestFiles()) + ctx.state.collectFiles(project, checker.getTestFiles()) await ctx.report('onCollected') } } diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 14cd5881a35f..554b16c69989 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -3,8 +3,8 @@ import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' import type { Vitest } from '../core' import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' +import type { TestProject } from '../project' import type { ResolvedConfig, SerializedConfig } from '../types/config' -import type { WorkspaceProject } from '../workspace' import EventEmitter from 'node:events' import * as nodeos from 'node:os' import { resolve } from 'node:path' @@ -19,7 +19,7 @@ import { createMethodsRPC } from './rpc' const suppressWarningsPath = resolve(rootDir, './suppress-warnings.cjs') -function createChildProcessChannel(project: WorkspaceProject) { +function createChildProcessChannel(project: TestProject) { const emitter = new EventEmitter() const cleanup = () => emitter.removeAllListeners() @@ -107,7 +107,7 @@ export function createVmForksPool( let id = 0 async function runFiles( - project: WorkspaceProject, + project: TestProject, config: SerializedConfig, files: string[], environment: ContextTestEnvironment, @@ -161,8 +161,8 @@ export function createVmForksPool( // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) - const configs = new Map() - const getConfig = (project: WorkspaceProject): SerializedConfig => { + const configs = new Map() + const getConfig = (project: TestProject): SerializedConfig => { if (configs.has(project)) { return configs.get(project)! } diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 01420cfc87be..4b5b72670403 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -3,9 +3,9 @@ import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' import type { Vitest } from '../core' import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' +import type { TestProject } from '../project' import type { ResolvedConfig, SerializedConfig } from '../types/config' import type { WorkerContext } from '../types/worker' -import type { WorkspaceProject } from '../workspace' import * as nodeos from 'node:os' import { resolve } from 'node:path' import { MessageChannel } from 'node:worker_threads' @@ -18,7 +18,7 @@ import { createMethodsRPC } from './rpc' const suppressWarningsPath = resolve(rootDir, './suppress-warnings.cjs') -function createWorkerChannel(project: WorkspaceProject) { +function createWorkerChannel(project: TestProject) { const channel = new MessageChannel() const port = channel.port2 const workerPort = channel.port1 @@ -98,7 +98,7 @@ export function createVmThreadsPool( let id = 0 async function runFiles( - project: WorkspaceProject, + project: TestProject, config: SerializedConfig, files: string[], environment: ContextTestEnvironment, @@ -156,8 +156,8 @@ export function createVmThreadsPool( // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) - const configs = new Map() - const getConfig = (project: WorkspaceProject): SerializedConfig => { + const configs = new Map() + const getConfig = (project: TestProject): SerializedConfig => { if (configs.has(project)) { return configs.get(project)! } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/project.ts similarity index 55% rename from packages/vitest/src/node/workspace.ts rename to packages/vitest/src/node/project.ts index 0fce02385324..1b4ca34363c8 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/project.ts @@ -1,4 +1,5 @@ import type { + ModuleNode, TransformResult, ViteDevServer, InlineConfig as ViteInlineConfig, @@ -7,7 +8,6 @@ import type { Typechecker } from '../typecheck/typechecker' import type { ProvidedContext } from '../types/general' import type { Vitest } from './core' import type { GlobalSetupFile } from './globalSetup' -import type { WorkspaceSpec as DeprecatedWorkspaceSpec } from './pool' import type { BrowserServer } from './types/browser' import type { ResolvedConfig, @@ -15,7 +15,7 @@ import type { UserConfig, UserWorkspaceConfig, } from './types/config' -import { promises as fs } from 'node:fs' +import { promises as fs, readFileSync } from 'node:fs' import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' @@ -38,95 +38,68 @@ import { loadGlobalSetupFiles } from './globalSetup' import { CoverageTransform } from './plugins/coverageTransform' import { MocksPlugins } from './plugins/mocks' import { WorkspaceVitestPlugin } from './plugins/workspace' -import { TestProject } from './reported-workspace-project' +import { type WorkspaceSpec as DeprecatedWorkspaceSpec, getFilePoolName } from './pool' import { TestSpecification } from './spec' import { createViteServer } from './vite' -interface InitializeProjectOptions extends UserWorkspaceConfig { - workspaceConfigPath: string - extends?: string -} - -export async function initializeProject( - workspacePath: string | number, - ctx: Vitest, - options: InitializeProjectOptions, -) { - const project = new WorkspaceProject(workspacePath, ctx, options) - - const { extends: extendsConfig, workspaceConfigPath, ...restOptions } = options - const root - = options.root - || (typeof workspacePath === 'number' - ? undefined - : workspacePath.endsWith('/') - ? workspacePath - : dirname(workspacePath)) - - const configFile = extendsConfig - ? resolve(dirname(workspaceConfigPath), extendsConfig) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath +export class TestProject { + /** + * The global Vitest instance. + * @experimental The public Vitest API is experimental and does not follow semver. + */ + public readonly vitest: Vitest - const config: ViteInlineConfig = { - ...restOptions, - root, - configFile, - // this will make "mode": "test" | "benchmark" inside defineConfig - mode: options.test?.mode || options.mode || ctx.config.mode, - plugins: [ - ...(options.plugins || []), - WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), - ], - } + /** + * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. + */ + public readonly globalConfig: ResolvedConfig - await createViteServer(config) + /** + * Browser instance if the browser is enabled. This is initialized when the tests run for the first time. + */ + public browser?: BrowserServer - return project -} + /** @deprecated use `vitest` instead */ + public ctx: Vitest -export class WorkspaceProject { - configOverride: Partial | undefined + /** + * Temporary directory for the project. This is unique for each project. Vitest stores transformed content here. + */ + public readonly tmpDir = join(tmpdir(), nanoid()) - config!: ResolvedConfig - server!: ViteDevServer vitenode!: ViteNodeServer runner!: ViteNodeRunner - browser?: BrowserServer typechecker?: Typechecker - closingPromise: Promise | undefined - - testFilesList: string[] | null = null - typecheckFilesList: string[] | null = null - - public testProject!: TestProject + private closingPromise: Promise | undefined - public readonly id = nanoid() - public readonly tmpDir = join(tmpdir(), this.id) + private testFilesList: string[] | null = null + private typecheckFilesList: string[] | null = null - private _globalSetups: GlobalSetupFile[] | undefined + private _globalSetups?: GlobalSetupFile[] private _provided: ProvidedContext = {} as any + private _config?: ResolvedConfig + private _vite?: ViteDevServer constructor( + /** @deprecated */ public path: string | number, - public ctx: Vitest, + vitest: Vitest, + /** @deprecated */ public options?: InitializeProjectOptions, - ) {} - - getName(): string { - return this.config.name || '' - } - - isCore() { - return this.ctx.getCoreWorkspaceProject() === this + ) { + this.vitest = vitest + this.ctx = vitest + this.globalConfig = vitest.config } + /** + * Provide a value to the test context. This value will be available to all tests with `inject`. + */ provide( key: T, value: ProvidedContext[T], - ) { + ): void { try { structuredClone(value) } @@ -138,26 +111,108 @@ export class WorkspaceProject { }, ) } + // casting `any` because the default type is `never` since `ProvidedContext` is empty (this._provided as any)[key] = value } + /** + * Get the provided context. The project context is merged with the global context. + */ getProvidedContext(): ProvidedContext { - if (this.isCore()) { + if (this.isRootProject()) { return this._provided } // globalSetup can run even if core workspace is not part of the test run // so we need to inherit its provided context return { - ...this.ctx.getCoreWorkspaceProject().getProvidedContext(), + ...this.vitest.getRootTestProject().getProvidedContext(), ...this._provided, } } + /** + * Creates a new test specification. Specifications describe how to run tests. + * @param moduleId The file path + */ + public createSpecification(moduleId: string, pool?: string): TestSpecification { + return new TestSpecification( + this, + moduleId, + pool || getFilePoolName(this, moduleId), + ) + } + + public toJSON(): SerializedTestProject { + return { + name: this.name, + serializedConfig: this.serializedConfig, + context: this.getProvidedContext(), + } + } + + /** + * Vite's dev server instance. Every workspace project has its own server. + */ + public get vite(): ViteDevServer { + if (!this._vite) { + throw new Error('The server was not set. It means that `project.vite` was called before the Vite server was established.') + } + return this._vite + } + + /** + * Resolved project configuration. + */ + public get config(): ResolvedConfig { + if (!this._config) { + throw new Error('The config was not set. It means that `project.config` was called before the Vite server was established.') + } + return this._config + } + + /** + * The name of the project or an empty string if not set. + */ + public get name(): string { + return this.config.name || '' + } + + /** + * Serialized project configuration. This is the config that tests receive. + */ + public get serializedConfig(): SerializedConfig { + return this._serializeOverridenConfig() + } + + /** @deprecated use `vite` instead */ + public get server(): ViteDevServer { + return this._vite! + } + + /** + * Check if this is the root project. The root project is the one that has the root config. + */ + public isRootProject(): boolean { + return this.vitest.getRootTestProject() === this + } + + /** @deprecated use `isRootProject` instead */ + public isCore(): boolean { + return this.isRootProject() + } + + /** @deprecated use `createSpecification` instead */ public createSpec(moduleId: string, pool: string): DeprecatedWorkspaceSpec { return new TestSpecification(this, moduleId, pool) as DeprecatedWorkspaceSpec } - async initializeGlobalSetup() { + /** @deprecated */ + initializeGlobalSetup() { + return this._initializeGlobalSetup() + } + + /** @internal */ + async _initializeGlobalSetup() { if (this._globalSetups) { return } @@ -171,7 +226,7 @@ export class WorkspaceProject { const teardown = await globalSetupFile.setup?.({ provide: (key, value) => this.provide(key, value), config: this.config, - onTestsRerun: cb => this.ctx.onTestsRerun(cb), + onTestsRerun: cb => this.vitest.onTestsRerun(cb), }) if (teardown == null || !!globalSetupFile.teardown) { continue @@ -185,7 +240,13 @@ export class WorkspaceProject { } } - async teardownGlobalSetup() { + /** @deprecated */ + teardownGlobalSetup() { + return this._teardownGlobalSetup() + } + + /** @internal */ + async _teardownGlobalSetup() { if (!this._globalSetups) { return } @@ -194,35 +255,53 @@ export class WorkspaceProject { } } + /** @deprecated use `vitest.logger` instead */ get logger() { - return this.ctx.logger + return this.vitest.logger } // it's possible that file path was imported with different queries (?raw, ?url, etc) - getModulesByFilepath(file: string) { + /** @deprecated use `.vite` or `.browser.vite` directly */ + getModulesByFilepath(file: string): Set { const set = this.server.moduleGraph.getModulesByFile(file) || this.browser?.vite.moduleGraph.getModulesByFile(file) return set || new Set() } - getModuleById(id: string) { + /** @deprecated use `.vite` or `.browser.vite` directly */ + getModuleById(id: string): ModuleNode | undefined { return ( this.server.moduleGraph.getModuleById(id) || this.browser?.vite.moduleGraph.getModuleById(id) ) } + /** @deprecated use `.vite` or `.browser.vite` directly */ getSourceMapModuleById(id: string): TransformResult['map'] | undefined { const mod = this.server.moduleGraph.getModuleById(id) return mod?.ssrTransformResult?.map || mod?.transformResult?.map } + /** @deprecated use `vitest.reporters` instead */ get reporters() { return this.ctx.reporters } - async globTestFiles(filters: string[] = []) { + /** + * Get all files in the project that match the globs in the config and the filters. + * @param filters String filters to match the test files. + */ + async globTestFiles(filters: string[] = []): Promise<{ + /** + * Test files that match the filters. + */ + testFiles: string[] + /** + * Typecheck test files that match the filters. This will be empty unless `typecheck.enabled` is `true`. + */ + typecheckTestFiles: string[] + }> { const dir = this.config.dir || this.config.root const { include, exclude, includeSource } = this.config @@ -253,12 +332,12 @@ export class WorkspaceProject { } } - async globAllTestFiles( + private async globAllTestFiles( include: string[], exclude: string[], includeSource: string[] | undefined, cwd: string, - ) { + ): Promise { if (this.testFilesList) { return this.testFilesList } @@ -272,7 +351,7 @@ export class WorkspaceProject { files.map(async (file) => { try { const code = await fs.readFile(file, 'utf-8') - if (this.isInSourceTestFile(code)) { + if (this.isInSourceTestCode(code)) { testFiles.push(file) } } @@ -288,14 +367,37 @@ export class WorkspaceProject { return testFiles } - isTestFile(id: string) { - return this.testFilesList && this.testFilesList.includes(id) + isBrowserEnabled(): boolean { + return isBrowserEnabled(this.config) + } + + /** @internal */ + _markTestFile(testPath: string): void { + this.testFilesList?.push(testPath) } - isTypecheckFile(id: string) { - return this.typecheckFilesList && this.typecheckFilesList.includes(id) + /** + * Returns if the file is a test file. Requires `.globTestFiles()` to be called first. + * @internal + */ + isTestFile(testPath: string): boolean { + return !!this.testFilesList && this.testFilesList.includes(testPath) } + /** + * Returns if the file is a typecheck test file. Requires `.globTestFiles()` to be called first. + * @internal + */ + isTypecheckFile(testPath: string): boolean { + return !!this.typecheckFilesList && this.typecheckFilesList.includes(testPath) + } + + /** @deprecated use `serializedConfig` instead */ + getSerializableConfig(): SerializedConfig { + return this._serializeOverridenConfig() + } + + /** @internal */ async globFiles(include: string[], exclude: string[], cwd: string) { const globOptions: fg.Options = { dot: true, @@ -310,8 +412,11 @@ export class WorkspaceProject { return files.map(file => slash(path.resolve(cwd, file))) } - async isTargetFile(id: string, source?: string): Promise { - const relativeId = relative(this.config.dir || this.config.root, id) + /** + * Test if a file matches the test globs. This does the actual glob matching unlike `isTestFile`. + */ + public matchesTestGlob(filepath: string, source?: string): boolean { + const relativeId = relative(this.config.dir || this.config.root, filepath) if (mm.isMatch(relativeId, this.config.exclude)) { return false } @@ -322,17 +427,22 @@ export class WorkspaceProject { this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource) ) { - source = source || (await fs.readFile(id, 'utf-8')) - return this.isInSourceTestFile(source) + const code = source || readFileSync(filepath, 'utf-8') + return this.isInSourceTestCode(code) } return false } - isInSourceTestFile(code: string) { + /** @deprecated use `matchesTestGlob` instead */ + async isTargetFile(id: string, source?: string): Promise { + return this.matchesTestGlob(id, source) + } + + private isInSourceTestCode(code: string): boolean { return code.includes('import.meta.vitest') } - filterFiles(testFiles: string[], filters: string[], dir: string) { + private filterFiles(testFiles: string[], filters: string[], dir: string): string[] { if (filters.length && process.platform === 'win32') { filters = filters.map(f => slash(f)) } @@ -360,15 +470,16 @@ export class WorkspaceProject { return testFiles } - async initBrowserServer() { + /** @internal */ + async _initBrowserServer() { if (!this.isBrowserEnabled() || this.browser) { return } - await this.ctx.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) + await this.vitest.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) const { createBrowserServer, distRoot } = await import('@vitest/browser') const browser = await createBrowserServer( this, - this.server.config.configFile, + this.vite.config.configFile, [ ...MocksPlugins({ filter(id) { @@ -383,44 +494,51 @@ export class WorkspaceProject { ) this.browser = browser if (this.config.browser.ui) { - setup(this.ctx, browser.vite) + setup(this.vitest, browser.vite) } } - static createBasicProject(ctx: Vitest) { - const project = new WorkspaceProject( - ctx.config.name || ctx.config.root, - ctx, - ) - project.vitenode = ctx.vitenode - project.server = ctx.server - project.runner = ctx.runner - project.config = ctx.config - for (const _providedKey in ctx.config.provide) { - const providedKey = _providedKey as keyof ProvidedContext - // type is very strict here, so we cast it to any - (project.provide as (key: string, value: unknown) => void)( - providedKey, - ctx.config.provide[providedKey], - ) + /** + * Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. + * If the resources are needed again, create a new project. + */ + public close(): Promise { + if (!this.closingPromise) { + this.closingPromise = Promise.all( + [ + this.vite?.close(), + this.typechecker?.stop(), + this.browser?.close(), + this.clearTmpDir(), + ].filter(Boolean), + ).then(() => { + this._provided = {} as any + this._vite = undefined + }) } - project.testProject = new TestProject(project) - return project + return this.closingPromise + } + + /** @deprecated use `name` instead */ + public getName(): string { + return this.config.name || '' } - static async createCoreProject(ctx: Vitest) { - return WorkspaceProject.createBasicProject(ctx) + /** @deprecated internal */ + public setServer(options: UserConfig, server: ViteDevServer) { + return this._configureServer(options, server) } - async setServer(options: UserConfig, server: ViteDevServer) { - this.config = resolveConfig( - this.ctx.mode, + /** @internal */ + async _configureServer(options: UserConfig, server: ViteDevServer): Promise { + this._config = resolveConfig( + this.vitest.mode, { ...options, - coverage: this.ctx.config.coverage, + coverage: this.vitest.config.coverage, }, server.config, - this.ctx.logger, + this.vitest.logger, ) for (const _providedKey in this.config.provide) { const providedKey = _providedKey as keyof ProvidedContext @@ -432,9 +550,8 @@ export class WorkspaceProject { } this.closingPromise = undefined - this.testProject = new TestProject(this) - this.server = server + this._vite = server this.vitenode = new ViteNodeServer(server, this.config.server) const node = this.vitenode @@ -450,54 +567,118 @@ export class WorkspaceProject { }) } - isBrowserEnabled(): boolean { - return isBrowserEnabled(this.config) - } - - getSerializableConfig(): SerializedConfig { + private _serializeOverridenConfig(): SerializedConfig { // TODO: serialize the config _once_ or when needed const config = serializeConfig( this.config, - this.ctx.config, - this.server.config, + this.vitest.config, + this.vite.config, ) - if (!this.ctx.configOverride) { + if (!this.vitest.configOverride) { return config } return deepMerge( config, - this.ctx.configOverride, + this.vitest.configOverride, ) } - close() { - if (!this.closingPromise) { - this.closingPromise = Promise.all( - [ - this.server?.close(), - this.typechecker?.stop(), - this.browser?.close(), - this.clearTmpDir(), - ].filter(Boolean), - ).then(() => (this._provided = {} as any)) - } - return this.closingPromise - } - - private async clearTmpDir() { + private async clearTmpDir(): Promise { try { await rm(this.tmpDir, { recursive: true }) } catch {} } - async initBrowserProvider() { + /** @deprecated */ + public initBrowserProvider(): Promise { + return this._initBrowserProvider() + } + + /** @internal */ + async _initBrowserProvider(): Promise { if (!this.isBrowserEnabled() || this.browser?.provider) { return } if (!this.browser) { - await this.initBrowserServer() + await this._initBrowserServer() } await this.browser?.initBrowserProvider() } + + /** @internal */ + static _createBasicProject(vitest: Vitest): TestProject { + const project = new TestProject( + vitest.config.name || vitest.config.root, + vitest, + ) + project.vitenode = vitest.vitenode + project.runner = vitest.runner + project._vite = vitest.server + project._config = vitest.config + for (const _providedKey in vitest.config.provide) { + const providedKey = _providedKey as keyof ProvidedContext + // type is very strict here, so we cast it to any + (project.provide as (key: string, value: unknown) => void)( + providedKey, + vitest.config.provide[providedKey], + ) + } + return project + } +} + +export { + /** @deprecated use `TestProject` instead */ + TestProject as WorkspaceProject, +} + +export interface SerializedTestProject { + name: string + serializedConfig: SerializedConfig + context: ProvidedContext +} + +interface InitializeProjectOptions extends UserWorkspaceConfig { + workspaceConfigPath: string + extends?: string +} + +export async function initializeProject( + workspacePath: string | number, + ctx: Vitest, + options: InitializeProjectOptions, +) { + const project = new TestProject(workspacePath, ctx, options) + + const { extends: extendsConfig, workspaceConfigPath, ...restOptions } = options + const root + = options.root + || (typeof workspacePath === 'number' + ? undefined + : workspacePath.endsWith('/') + ? workspacePath + : dirname(workspacePath)) + + const configFile = extendsConfig + ? resolve(dirname(workspaceConfigPath), extendsConfig) + : typeof workspacePath === 'number' || workspacePath.endsWith('/') + ? false + : workspacePath + + const config: ViteInlineConfig = { + ...restOptions, + root, + configFile, + // this will make "mode": "test" | "benchmark" inside defineConfig + mode: options.test?.mode || options.mode || ctx.config.mode, + plugins: [ + ...(options.plugins || []), + WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), + ], + } + + await createViteServer(config) + + return project } diff --git a/packages/vitest/src/node/reported-workspace-project.ts b/packages/vitest/src/node/reported-workspace-project.ts deleted file mode 100644 index 06548785df7f..000000000000 --- a/packages/vitest/src/node/reported-workspace-project.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { ViteDevServer } from 'vite' -import type { ProvidedContext } from '../types/general' -import type { Vitest } from './core' -import type { ResolvedConfig, ResolvedProjectConfig, SerializedConfig } from './types/config' -import type { WorkspaceProject } from './workspace' - -export class TestProject { - /** - * The global vitest instance. - * @experimental The public Vitest API is experimental and does not follow semver. - */ - public readonly vitest: Vitest - /** - * The workspace project this test project is associated with. - * @experimental The public Vitest API is experimental and does not follow semver. - */ - public readonly workspaceProject: WorkspaceProject - - /** - * Vite's dev server instance. Every workspace project has its own server. - */ - public readonly vite: ViteDevServer - /** - * Resolved project configuration. - */ - public readonly config: ResolvedProjectConfig - /** - * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. - */ - public readonly globalConfig: ResolvedConfig - - /** - * The name of the project or an empty string if not set. - */ - public readonly name: string - - constructor(workspaceProject: WorkspaceProject) { - this.workspaceProject = workspaceProject - this.vitest = workspaceProject.ctx - this.vite = workspaceProject.server - this.globalConfig = workspaceProject.ctx.config - this.config = workspaceProject.config - this.name = workspaceProject.getName() - } - - /** - * Serialized project configuration. This is the config that tests receive. - */ - public get serializedConfig() { - return this.workspaceProject.getSerializableConfig() - } - - /** - * Custom context provided to the project. - */ - public context(): ProvidedContext { - return this.workspaceProject.getProvidedContext() - } - - /** - * Provide a custom serializable context to the project. This context will be available for tests once they run. - */ - public provide( - key: T, - value: ProvidedContext[T], - ): void { - this.workspaceProject.provide(key, value) - } - - public toJSON(): SerializedTestProject { - return { - name: this.name, - serializedConfig: this.serializedConfig, - context: this.context(), - } - } -} - -export interface SerializedTestProject { - name: string - serializedConfig: SerializedConfig - context: ProvidedContext -} diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index f4b2d7f4596b..44e7ccc3ed44 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -290,7 +290,7 @@ export abstract class BaseReporter implements Reporter { const project = log.taskId ? this.ctx.getProjectByTaskId(log.taskId) - : this.ctx.getCoreWorkspaceProject() + : this.ctx.getRootTestProject() const stack = log.browser ? (project.browser?.parseStacktrace(log.origin) || []) diff --git a/packages/vitest/src/node/reporters/blob.ts b/packages/vitest/src/node/reporters/blob.ts index d98f94bde6d1..da7678c1701a 100644 --- a/packages/vitest/src/node/reporters/blob.ts +++ b/packages/vitest/src/node/reporters/blob.ts @@ -1,7 +1,7 @@ import type { File } from '@vitest/runner' import type { Vitest } from '../core' +import type { TestProject } from '../project' import type { Reporter } from '../types/reporter' -import type { WorkspaceProject } from '../workspace' import { existsSync } from 'node:fs' import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises' import { parse, stringify } from 'flatted' @@ -46,7 +46,7 @@ export class BlobReporter implements Reporter { (project) => { return [ project.getName(), - [...project.server.moduleGraph.idToModuleMap.entries()].map((mod) => { + [...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => { if (!mod[1].file) { return null } @@ -79,7 +79,7 @@ export class BlobReporter implements Reporter { export async function readBlobs( currentVersion: string, blobsDirectory: string, - projectsArray: WorkspaceProject[], + projectsArray: TestProject[], ) { // using process.cwd() because --merge-reports can only be used in CLI const resolvedDir = resolve(process.cwd(), blobsDirectory) @@ -136,10 +136,10 @@ export async function readBlobs( return } moduleIds.forEach(([moduleId, file, url]) => { - const moduleNode = project.server.moduleGraph.createFileOnlyEntry(file) + const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file) moduleNode.url = url moduleNode.id = moduleId - project.server.moduleGraph.idToModuleMap.set(moduleId, moduleNode) + project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode) }) }) }) diff --git a/packages/vitest/src/node/reporters/github-actions.ts b/packages/vitest/src/node/reporters/github-actions.ts index 111786604f59..7d63b7a0bcc2 100644 --- a/packages/vitest/src/node/reporters/github-actions.ts +++ b/packages/vitest/src/node/reporters/github-actions.ts @@ -1,7 +1,7 @@ import type { File } from '@vitest/runner' import type { Vitest } from '../core' +import type { TestProject } from '../project' import type { Reporter } from '../types/reporter' -import type { WorkspaceProject } from '../workspace' import { stripVTControlCharacters } from 'node:util' import { getFullName, getTasks } from '@vitest/runner/utils' import { capturePrintError } from '../error' @@ -16,14 +16,14 @@ export class GithubActionsReporter implements Reporter { onFinished(files: File[] = [], errors: unknown[] = []) { // collect all errors and associate them with projects const projectErrors = new Array<{ - project: WorkspaceProject + project: TestProject title: string error: unknown file?: File }>() for (const error of errors) { projectErrors.push({ - project: this.ctx.getCoreWorkspaceProject(), + project: this.ctx.getRootTestProject(), title: 'Unhandled error', error, }) diff --git a/packages/vitest/src/node/reporters/index.ts b/packages/vitest/src/node/reporters/index.ts index dbeac3434e1c..f1c1c7449df1 100644 --- a/packages/vitest/src/node/reporters/index.ts +++ b/packages/vitest/src/node/reporters/index.ts @@ -30,7 +30,7 @@ export { } export type { BaseReporter, Reporter } -export type { TestProject } from '../reported-workspace-project' +export type { TestProject } from '../project' /** * @deprecated Use `TestModule` instead */ diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 9cc4c6b3cbd2..973570d6c4f4 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -7,8 +7,7 @@ import type { TaskMeta, } from '@vitest/runner' import type { TestError } from '@vitest/utils' -import type { WorkspaceProject } from '../workspace' -import { TestProject } from '../reported-workspace-project' +import type { TestProject } from '../project' class ReportedTaskImplementation { /** @@ -36,10 +35,10 @@ class ReportedTaskImplementation { protected constructor( task: RunnerTask, - project: WorkspaceProject, + project: TestProject, ) { this.task = task - this.project = project.testProject || (project.testProject = new TestProject(project)) + this.project = project this.id = task.id this.location = task.location } @@ -47,7 +46,7 @@ class ReportedTaskImplementation { /** * Creates a new reported task instance and stores it in the project's state for future use. */ - static register(task: RunnerTask, project: WorkspaceProject) { + static register(task: RunnerTask, project: TestProject) { const state = new this(task, project) as TestCase | TestSuite | TestModule storeTask(project, task, state) return state @@ -80,7 +79,7 @@ export class TestCase extends ReportedTaskImplementation { */ public readonly parent: TestSuite | TestModule - protected constructor(task: RunnerTestCase | RunnerCustomCase, project: WorkspaceProject) { + protected constructor(task: RunnerTestCase | RunnerCustomCase, project: TestProject) { super(task, project) this.name = task.name @@ -184,9 +183,9 @@ export class TestCase extends ReportedTaskImplementation { class TestCollection { #task: RunnerTestSuite | RunnerTestFile - #project: WorkspaceProject + #project: TestProject - constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { + constructor(task: RunnerTestSuite | RunnerTestFile, project: TestProject) { this.#task = task this.#project = project } @@ -296,7 +295,7 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { */ public readonly children: TestCollection - protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { + protected constructor(task: RunnerTestSuite | RunnerTestFile, project: TestProject) { super(task, project) this.children = new TestCollection(task, project) } @@ -328,7 +327,7 @@ export class TestSuite extends SuiteImplementation { */ public readonly options: TaskOptions - protected constructor(task: RunnerTestSuite, project: WorkspaceProject) { + protected constructor(task: RunnerTestSuite, project: TestProject) { super(task, project) this.name = task.name @@ -371,7 +370,7 @@ export class TestModule extends SuiteImplementation { */ public readonly moduleId: string - protected constructor(task: RunnerTestFile, project: WorkspaceProject) { + protected constructor(task: RunnerTestFile, project: TestProject) { super(task, project) this.moduleId = task.filepath } @@ -523,18 +522,18 @@ function getTestState(test: TestCase): TestResult['state'] | 'running' { } function storeTask( - project: WorkspaceProject, + project: TestProject, runnerTask: RunnerTask, reportedTask: TestCase | TestSuite | TestModule, ): void { - project.ctx.state.reportedTasksMap.set(runnerTask, reportedTask) + project.vitest.state.reportedTasksMap.set(runnerTask, reportedTask) } function getReportedTask( - project: WorkspaceProject, + project: TestProject, runnerTask: RunnerTask, ): TestCase | TestSuite | TestModule { - const reportedTask = project.ctx.state.getReportedEntity(runnerTask) + const reportedTask = project.vitest.state.getReportedEntity(runnerTask) if (!reportedTask) { throw new Error( `Task instance was not found for ${runnerTask.type} "${runnerTask.name}"`, diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index fad253934adc..96e12a8580fd 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -1,13 +1,12 @@ import type { SerializedTestSpecification } from '../runtime/types/utils' -import type { TestProject } from './reported-workspace-project' +import type { TestProject } from './project' import type { Pool } from './types/pool-options' -import type { WorkspaceProject } from './workspace' export class TestSpecification { /** * @deprecated use `project` instead */ - public readonly 0: WorkspaceProject + public readonly 0: TestProject /** * @deprecated use `moduleId` instead */ @@ -23,15 +22,15 @@ export class TestSpecification { // public readonly location: WorkspaceSpecLocation | undefined constructor( - workspaceProject: WorkspaceProject, + project: TestProject, moduleId: string, pool: Pool, // location?: WorkspaceSpecLocation | undefined, ) { - this[0] = workspaceProject + this[0] = project this[1] = moduleId this[2] = { pool } - this.project = workspaceProject.testProject + this.project = project this.moduleId = moduleId this.pool = pool // this.location = location @@ -53,7 +52,7 @@ export class TestSpecification { * @deprecated */ *[Symbol.iterator]() { - yield this.project.workspaceProject + yield this.project yield this.moduleId yield this.pool } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index efeef50f87d7..6903b6084f85 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -1,6 +1,6 @@ import type { File, Task, TaskResultPack } from '@vitest/runner' import type { UserConsoleLog } from '../types/general' -import type { WorkspaceProject } from './workspace' +import type { TestProject } from './project' import { createFileTask } from '@vitest/runner/utils' import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks' @@ -90,6 +90,10 @@ export class StateManager { }) } + getTestModules(keys?: string[]): TestModule[] { + return this.getFiles(keys).map(file => this.getReportedEntity(file) as TestModule) + } + getFilepaths(): string[] { return Array.from(this.filesMap.keys()) } @@ -106,7 +110,7 @@ export class StateManager { }) } - collectFiles(project: WorkspaceProject, files: File[] = []) { + collectFiles(project: TestProject, files: File[] = []) { files.forEach((file) => { const existing = this.filesMap.get(file.filepath) || [] const otherFiles = existing.filter( @@ -127,7 +131,7 @@ export class StateManager { } clearFiles( - project: WorkspaceProject, + project: TestProject, paths: string[] = [], ) { paths.forEach((path) => { @@ -157,7 +161,7 @@ export class StateManager { }) } - updateId(task: Task, project: WorkspaceProject) { + updateId(task: Task, project: TestProject) { if (this.idMap.get(task.id) === task) { return } @@ -214,7 +218,7 @@ export class StateManager { ).length } - cancelFiles(files: string[], project: WorkspaceProject) { + cancelFiles(files: string[], project: TestProject) { this.collectFiles( project, files.map(filepath => diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 243fdfcbc614..19e2b9f2a6df 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -2,7 +2,7 @@ import type { CancelReason } from '@vitest/runner' import type { Awaitable, ErrorWithDiff, ParsedStack } from '@vitest/utils' import type { StackTraceParserOptions } from '@vitest/utils/source-map' import type { ViteDevServer } from 'vite' -import type { WorkspaceProject } from '../workspace' +import type { TestProject } from '../project' import type { ApiConfig } from './config' export interface BrowserProviderInitializationOptions { @@ -32,7 +32,7 @@ export interface BrowserProvider { close: () => Awaitable // eslint-disable-next-line ts/method-signature-style -- we want to allow extended options initialize( - ctx: WorkspaceProject, + ctx: TestProject, options: BrowserProviderInitializationOptions ): Awaitable } @@ -180,7 +180,7 @@ export interface BrowserConfigOptions { export interface BrowserCommandContext { testPath: string | undefined provider: BrowserProvider - project: WorkspaceProject + project: TestProject contextId: string } diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index fe600405ddc1..724e1773992b 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1101,7 +1101,8 @@ export type ProjectConfig = Omit< export type ResolvedProjectConfig = Omit< ResolvedConfig, - NonProjectOptions + // some options cannot be set, but they are inherited from the workspace + Exclude > export interface UserWorkspaceConfig extends ViteUserConfig { @@ -1116,10 +1117,13 @@ export type UserProjectConfigExport = | Promise | UserProjectConfigFn -export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { +export type TestProjectConfiguration = string | (UserProjectConfigExport & { /** * Relative path to the extendable config. All other options will be merged with this config. * @example '../vite.config.ts' */ extends?: string }) + +/** @deprecated use `TestProjectConfiguration` instead */ +export type WorkspaceProjectConfiguration = TestProjectConfiguration diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 4f7be66f12b2..d826b6915054 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -1,6 +1,6 @@ import type { Vitest } from '../core' -import type { UserConfig, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../types/config' -import type { WorkspaceProject } from '../workspace' +import type { TestProject } from '../project' +import type { TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' @@ -8,16 +8,16 @@ import fg from 'fast-glob' import { relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' -import { initializeProject } from '../workspace' +import { initializeProject } from '../project' import { isDynamicPattern } from './fast-glob-pattern' export async function resolveWorkspace( vitest: Vitest, cliOptions: UserConfig, workspaceConfigPath: string, - workspaceDefinition: WorkspaceProjectConfiguration[], -): Promise { - const { configFiles, projectConfigs, nonConfigDirectories } = await resolveWorkspaceProjectConfigs( + workspaceDefinition: TestProjectConfiguration[], +): Promise { + const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs( vitest, workspaceConfigPath, workspaceDefinition, @@ -50,7 +50,7 @@ export async function resolveWorkspace( return acc }, {} as UserConfig) - const projectPromises: Promise[] = [] + const projectPromises: Promise[] = [] const fileProjects = [...configFiles, ...nonConfigDirectories] const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) @@ -100,9 +100,9 @@ export async function resolveWorkspace( : [' '] throw new Error([ `Project name "${name}"`, - project.server.config.configFile ? ` from "${relative(vitest.config.root, project.server.config.configFile)}"` : '', + project.vite.config.configFile ? ` from "${relative(vitest.config.root, project.vite.config.configFile)}"` : '', ' is not unique.', - duplicate?.server.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '', + duplicate?.vite.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.vite.config.configFile)}".` : '', filesError, 'All projects in a workspace should have unique names. Make sure your configuration is correct.', ].join('')) @@ -113,10 +113,10 @@ export async function resolveWorkspace( return resolvedProjects } -async function resolveWorkspaceProjectConfigs( +async function resolveTestProjectConfigs( vitest: Vitest, workspaceConfigPath: string, - workspaceDefinition: WorkspaceProjectConfiguration[], + workspaceDefinition: TestProjectConfiguration[], ) { // project configurations that were specified directly const projectsOptions: UserWorkspaceConfig[] = [] diff --git a/packages/vitest/src/public/config.ts b/packages/vitest/src/public/config.ts index 9f8b02110354..fd906108cb68 100644 --- a/packages/vitest/src/public/config.ts +++ b/packages/vitest/src/public/config.ts @@ -1,6 +1,6 @@ import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite' -import type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config' +import type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config' import '../node/types/vite' export { extraInlineDeps } from '../constants' @@ -20,7 +20,7 @@ export type { ConfigEnv, ViteUserConfig } * @deprecated Use `ViteUserConfig` instead */ export type UserConfig = ViteUserConfig -export type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } +export type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig export type UserConfigFnPromise = (env: ConfigEnv) => Promise export type UserConfigFn = ( @@ -51,6 +51,6 @@ export function defineProject(config: UserProjectConfigExport): UserProjectConfi return config } -export function defineWorkspace(config: WorkspaceProjectConfiguration[]): WorkspaceProjectConfiguration[] { +export function defineWorkspace(config: TestProjectConfiguration[]): TestProjectConfiguration[] { return config } diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index c8ba9ef7679f..cf8485ced728 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -15,8 +15,8 @@ export { resolveFsAllow } from '../node/plugins/utils' export type { ProcessPool, WorkspaceSpec } from '../node/pool' export { getFilePoolName } from '../node/pool' export { createMethodsRPC } from '../node/pools/rpc' -export { TestProject } from '../node/reported-workspace-project' -export type { SerializedTestProject } from '../node/reported-workspace-project' +export type { SerializedTestProject, TestProject } from '../node/project' +export type { WorkspaceProject } from '../node/project' export type { HTMLOptions } from '../node/reporters/html' export type { JsonOptions } from '../node/reporters/json' @@ -110,7 +110,6 @@ export type { export const TestFile = _TestFile export type { WorkerContext } from '../node/types/worker' export { createViteLogger } from '../node/viteLogger' -export type { WorkspaceProject } from '../node/workspace' /** * @deprecated Use `ModuleDiagnostic` instead diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts index a3d4ed864dd8..91088b97ec1d 100644 --- a/packages/vitest/src/typecheck/collect.ts +++ b/packages/vitest/src/typecheck/collect.ts @@ -1,6 +1,6 @@ import type { File, Suite, Test } from '@vitest/runner' import type { RawSourceMap } from 'vite-node' -import type { WorkspaceProject } from '../node/workspace' +import type { TestProject } from '../node/project' import { calculateSuiteHash, generateHash, @@ -44,7 +44,7 @@ export interface FileInformation { } export async function collectTests( - ctx: WorkspaceProject, + ctx: TestProject, filepath: string, ): Promise { const request = await ctx.vitenode.transformRequest(filepath, filepath) diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index 22fcd4c9b866..5b8710ee59d5 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -3,7 +3,7 @@ import type { File, Task, TaskResultPack, TaskState } from '@vitest/runner' import type { ParsedStack } from '@vitest/utils' import type { ChildProcess } from 'node:child_process' import type { Vitest } from '../node/core' -import type { WorkspaceProject } from '../node/workspace' +import type { TestProject } from '../node/project' import type { Awaitable } from '../types/general' import type { FileInformation } from './collect' import type { TscErrorInfo } from './types' @@ -54,7 +54,7 @@ export class Typechecker { protected files: string[] = [] - constructor(protected ctx: WorkspaceProject) {} + constructor(protected ctx: TestProject) {} public setFiles(files: string[]) { this.files = files diff --git a/packages/vitest/src/utils/graph.ts b/packages/vitest/src/utils/graph.ts index 57cab5b14d45..3e3cd783524e 100644 --- a/packages/vitest/src/utils/graph.ts +++ b/packages/vitest/src/utils/graph.ts @@ -51,7 +51,7 @@ export async function getModuleGraph( await get(project.browser.vite.moduleGraph.getModuleById(id)) } else { - await get(project.server.moduleGraph.getModuleById(id)) + await get(project.vite.moduleGraph.getModuleById(id)) } return { diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index 57b122c6fc35..2d9c44d994b1 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -32,7 +32,7 @@ export async function groupFilesByEnv( const filesWithEnv = await Promise.all( files.map(async (spec) => { const file = spec.moduleId - const project = spec.project.workspaceProject + const project = spec.project const code = await fs.readFile(file, 'utf-8') // 1. Check for control comments in the file diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 264b1e033b23..15b0b768ef84 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -35,7 +35,7 @@ beforeAll(async () => { logHeapUsage: true, }) state = ctx!.state - project = ctx!.getCoreWorkspaceProject() + project = ctx!.getRootTestProject() files = state.getFiles() expect(files).toHaveLength(1) testModule = state.getReportedEntity(files[0])! as TestModule @@ -55,7 +55,7 @@ it('correctly reports a file', () => { expect(testModule.id).toBe(files[0].id) expect(testModule.location).toBeUndefined() expect(testModule.moduleId).toBe(resolve(root, './1_first.test.ts')) - expect(testModule.project.workspaceProject).toBe(project) + expect(testModule.project).toBe(project) expect(testModule.children.size).toBe(14) const tests = [...testModule.children.tests()] diff --git a/test/core/test/sequencers.test.ts b/test/core/test/sequencers.test.ts index 28403b042434..0850cd5f44cb 100644 --- a/test/core/test/sequencers.test.ts +++ b/test/core/test/sequencers.test.ts @@ -19,9 +19,7 @@ function buildCtx() { function buildWorkspace() { return { - testProject: { - name: 'test', - }, + name: 'test', } as any as WorkspaceProject } diff --git a/test/coverage-test/test/threshold-100.test.ts b/test/coverage-test/test/threshold-100.test.ts index 8c5a3e261cab..9edf7ae7efaa 100644 --- a/test/coverage-test/test/threshold-100.test.ts +++ b/test/coverage-test/test/threshold-100.test.ts @@ -20,7 +20,7 @@ test('{ threshold: { 100: true }}', async () => { 'verbose', { onInit(ctx) { - ctx.getCoreWorkspaceProject().provide('coverage', { + ctx.getRootTestProject().provide('coverage', { provider: ctx.config.coverage.provider, thresholds: (ctx.config.coverage as any).thresholds, }) diff --git a/tsconfig.base.json b/tsconfig.base.json index 0b7c334df5be..28a1fd4b24c6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,7 @@ "strict": true, "declaration": true, "noEmit": true, + "stripInternal": true, "esModuleInterop": true, "skipLibCheck": true }