vitest 4.0.17 → 4.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE.md +36 -0
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +2 -2
  4. package/dist/chunks/acorn.B2iPLyUM.js +5958 -0
  5. package/dist/chunks/{base.XJJQZiKB.js → base.DiopZV8F.js} +49 -14
  6. package/dist/chunks/{benchmark.B3N2zMcH.js → benchmark.BoqSLF53.js} +1 -1
  7. package/dist/chunks/{browser.d.ChKACdzH.d.ts → browser.d.BE4kbYok.d.ts} +4 -1
  8. package/dist/chunks/{cac.jRCLJDDc.js → cac.C4jjt2RX.js} +816 -14
  9. package/dist/chunks/{cli-api.Cx2DW4Bc.js → cli-api.ChbI1JU9.js} +412 -166
  10. package/dist/chunks/{config.d.Cy95HiCx.d.ts → config.d.Cr1Ep39N.d.ts} +13 -11
  11. package/dist/chunks/{console.Cf-YriPC.js → console.CNlG1KsP.js} +3 -2
  12. package/dist/chunks/{constants.D_Q9UYh-.js → constants.B63TT-Bl.js} +1 -1
  13. package/dist/chunks/coverage.tyqbzn4W.js +1001 -0
  14. package/dist/chunks/{creator.DAmOKTvJ.js → creator.yyCHuw5R.js} +33 -2
  15. package/dist/chunks/{global.d.B15mdLcR.d.ts → global.d.JeWMqlOm.d.ts} +1 -1
  16. package/dist/chunks/{globals.DOayXfHP.js → globals.C6Ecf1TO.js} +11 -10
  17. package/dist/chunks/{index.M8mOzt4Y.js → index.B-iBE_Gx.js} +21 -5
  18. package/dist/chunks/{coverage.AVPTjMgw.js → index.BCY_7LL2.js} +5 -959
  19. package/dist/chunks/{index.6Qv1eEA6.js → index.CAN630q3.js} +20 -8
  20. package/dist/chunks/{index.C5r1PdPD.js → index.CFulQRmC.js} +1 -1
  21. package/dist/chunks/{index.Z5E_ObnR.js → index.CouFDptX.js} +4 -2
  22. package/dist/chunks/{init-forks.BC6ZwHQN.js → init-forks.BnCXPazU.js} +1 -1
  23. package/dist/chunks/{init-threads.CxSxLC0N.js → init-threads.Cyh2PqXi.js} +1 -1
  24. package/dist/chunks/{init.C9kljSTm.js → init.B95Mm0Iz.js} +65 -12
  25. package/dist/chunks/native.mV0-490A.js +148 -0
  26. package/dist/chunks/nativeModuleMocker.D_q5sFv6.js +206 -0
  27. package/dist/chunks/nativeModuleRunner.BIakptoF.js +36 -0
  28. package/dist/chunks/{node.Ce0vMQM7.js → node.CrSEwhm4.js} +1 -1
  29. package/dist/chunks/{plugin.d.CtqpEehP.d.ts → plugin.d.C9o5bttz.d.ts} +1 -1
  30. package/dist/chunks/{reporters.d.CWXNI2jG.d.ts → reporters.d.7faYdkxy.d.ts} +146 -49
  31. package/dist/chunks/rpc.DcRWTy5G.js +148 -0
  32. package/dist/chunks/{rpc.d.RH3apGEf.d.ts → rpc.d.CM7x9-sm.d.ts} +1 -0
  33. package/dist/chunks/{setup-common.Cm-kSBVi.js → setup-common.cvFp-ao9.js} +2 -2
  34. package/dist/chunks/{startModuleRunner.DEj0jb3e.js → startVitestModuleRunner.BK-u7y4N.js} +182 -391
  35. package/dist/chunks/{vi.2VT5v0um.js → test.G82XYNFk.js} +505 -119
  36. package/dist/chunks/{utils.DvEY5TfP.js → utils.DT4VyRyl.js} +5 -1
  37. package/dist/chunks/{vm.CMjifoPa.js → vm.BdLtzhnj.js} +15 -11
  38. package/dist/chunks/{worker.d.Dyxm8DEL.d.ts → worker.d.CPzI2ZzJ.d.ts} +2 -2
  39. package/dist/cli.js +4 -3
  40. package/dist/config.d.ts +11 -11
  41. package/dist/config.js +1 -1
  42. package/dist/coverage.d.ts +10 -8
  43. package/dist/coverage.js +7 -4
  44. package/dist/environments.js +2 -0
  45. package/dist/index.d.ts +30 -23
  46. package/dist/index.js +9 -8
  47. package/dist/module-evaluator.d.ts +10 -1
  48. package/dist/module-evaluator.js +1 -5
  49. package/dist/node.d.ts +13 -12
  50. package/dist/node.js +27 -25
  51. package/dist/nodejs-worker-loader.js +41 -0
  52. package/dist/reporters.d.ts +8 -8
  53. package/dist/reporters.js +4 -2
  54. package/dist/runners.d.ts +24 -4
  55. package/dist/runners.js +6 -6
  56. package/dist/runtime.d.ts +6 -0
  57. package/dist/runtime.js +35 -0
  58. package/dist/snapshot.js +4 -2
  59. package/dist/suite.js +4 -2
  60. package/dist/worker.d.ts +8 -7
  61. package/dist/worker.js +25 -20
  62. package/dist/workers/forks.js +21 -16
  63. package/dist/workers/runVmTests.js +11 -13
  64. package/dist/workers/threads.js +21 -16
  65. package/dist/workers/vmForks.js +14 -11
  66. package/dist/workers/vmThreads.js +14 -11
  67. package/package.json +28 -29
  68. package/suppress-warnings.cjs +1 -0
  69. package/dist/chunks/date.Bq6ZW5rf.js +0 -73
  70. package/dist/chunks/rpc.BoxB0q7B.js +0 -76
  71. package/dist/chunks/test.B8ej_ZHS.js +0 -254
  72. package/dist/mocker.d.ts +0 -1
  73. package/dist/mocker.js +0 -1
  74. package/dist/module-runner.js +0 -17
@@ -2,20 +2,20 @@ import fs, { promises, existsSync, mkdirSync, readFileSync, statSync, readdirSyn
2
2
  import { relative, resolve, dirname, join, extname, normalize, basename, isAbsolute } from 'pathe';
3
3
  import { C as CoverageProviderMap } from './coverage.D_JHT54q.js';
4
4
  import path, { resolve as resolve$1 } from 'node:path';
5
- import { noop, createDefer, slash, withTrailingSlash, cleanUrl, wrapId, isExternalUrl, unwrapId, toArray, deepMerge, nanoid, deepClone, isPrimitive, notNullish } from '@vitest/utils/helpers';
5
+ import { cleanUrl, noop, unique, createDefer, slash, withTrailingSlash, wrapId, isExternalUrl, unwrapId, toArray, deepMerge, nanoid, deepClone, isPrimitive, notNullish } from '@vitest/utils/helpers';
6
6
  import { a as any, p as prompt } from './index.D4KonVSU.js';
7
- import { h as hash, R as RandomSequencer, i as isPackageExists, c as isBrowserEnabled, r as resolveConfig, g as getCoverageProvider, a as resolveApiServerConfig, d as resolveModule } from './coverage.AVPTjMgw.js';
7
+ import { i as isPackageExists, r as resolveModule } from './index.BCY_7LL2.js';
8
8
  import * as vite from 'vite';
9
- import { isFileServingAllowed as isFileServingAllowed$1, parseAst, searchForWorkspaceRoot, fetchModule, version, mergeConfig, createServer, isFileLoadingAllowed, normalizePath } from 'vite';
10
- import { A as API_PATH, c as configFiles, d as defaultBrowserPort, a as defaultPort } from './constants.D_Q9UYh-.js';
9
+ import { createServer, isFileLoadingAllowed, normalizePath, parseAst, searchForWorkspaceRoot, fetchModule, version, mergeConfig } from 'vite';
10
+ import { A as API_PATH, c as configFiles, d as defaultBrowserPort, b as defaultPort } from './constants.B63TT-Bl.js';
11
11
  import * as nodeos from 'node:os';
12
12
  import nodeos__default, { tmpdir } from 'node:os';
13
- import { generateHash as generateHash$1, createTaskName, calculateSuiteHash, someTasksAreOnly, interpretTaskModes, hasFailed, generateFileHash, limitConcurrency, createFileTask as createFileTask$1, getTasks, isTestCase } from '@vitest/runner/utils';
13
+ import { generateHash as generateHash$1, createTaskName, validateTags, calculateSuiteHash, someTasksAreOnly, interpretTaskModes, hasFailed, generateFileHash, limitConcurrency, createFileTask as createFileTask$1, getTasks, isTestCase } from '@vitest/runner/utils';
14
14
  import { SnapshotManager } from '@vitest/snapshot/manager';
15
- import { v as version$1 } from './cac.jRCLJDDc.js';
15
+ import { v as version$1 } from './cac.C4jjt2RX.js';
16
16
  import { performance as performance$1 } from 'node:perf_hooks';
17
17
  import { c as createBirpc } from './index.Chj8NDwU.js';
18
- import { p as parse, d as stringify, e as createIndexLocationsMap, h as TraceMap, o as originalPositionFor, i as ancestor, j as printError, f as formatProjectName, w as withLabel, k as errorBanner, l as divider, m as Typechecker, n as generateCodeFrame, q as escapeRegExp, r as createDefinesScript, R as ReportersMap, u as groupBy, B as BlobReporter, v as readBlobs, x as convertTasksToEvents, H as HangingProcessReporter, y as wildcardPatternToRegExp, z as stdout } from './index.M8mOzt4Y.js';
18
+ import { p as parse, d as stringify, e as createIndexLocationsMap, h as TraceMap, o as originalPositionFor, i as ancestor, j as printError, f as formatProjectName, w as withLabel, k as errorBanner, l as divider, m as Typechecker, n as generateCodeFrame, q as escapeRegExp, r as createDefinesScript, R as ReportersMap, u as groupBy, B as BlobReporter, v as readBlobs, x as convertTasksToEvents, H as HangingProcessReporter, y as wildcardPatternToRegExp, z as stdout } from './index.B-iBE_Gx.js';
19
19
  import require$$0$3 from 'events';
20
20
  import require$$1$1 from 'https';
21
21
  import require$$2 from 'http';
@@ -29,8 +29,10 @@ import require$$0$1 from 'buffer';
29
29
  import { g as getDefaultExportFromCjs } from './_commonjsHelpers.D26ty3Ew.js';
30
30
  import crypto, { createHash } from 'node:crypto';
31
31
  import { rootDir, distDir } from '../path.js';
32
+ import { N as NativeModuleRunner } from './nativeModuleRunner.BIakptoF.js';
32
33
  import { T as Traces } from './traces.CCmnQaNT.js';
33
34
  import { createDebug } from 'obug';
35
+ import { h as hash, R as RandomSequencer, i as isBrowserEnabled, r as resolveConfig, g as getCoverageProvider, a as resolveApiServerConfig } from './coverage.tyqbzn4W.js';
34
36
  import { rm, readFile, writeFile, rename, stat, unlink, mkdir, copyFile } from 'node:fs/promises';
35
37
  import c from 'tinyrainbow';
36
38
  import { VitestModuleEvaluator } from '#module-evaluator';
@@ -53,7 +55,7 @@ import { c as configDefaults } from './defaults.BOqNVLsY.js';
53
55
  import { KNOWN_ASSET_RE } from '@vitest/utils/constants';
54
56
  import { findNearestPackageData } from '@vitest/utils/resolver';
55
57
  import * as esModuleLexer from 'es-module-lexer';
56
- import { a as BenchmarkReportsMap } from './index.C5r1PdPD.js';
58
+ import { a as BenchmarkReportsMap } from './index.CFulQRmC.js';
57
59
  import assert$1 from 'node:assert';
58
60
  import { serializeValue } from '@vitest/utils/serialize';
59
61
  import { parseErrorStacktrace } from '@vitest/utils/source-map';
@@ -76,6 +78,7 @@ function requireConstants () {
76
78
 
77
79
  constants = {
78
80
  BINARY_TYPES,
81
+ CLOSE_TIMEOUT: 30000,
79
82
  EMPTY_BUFFER: Buffer.alloc(0),
80
83
  GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
81
84
  hasBlob,
@@ -2846,6 +2849,7 @@ function requireWebsocket () {
2846
2849
 
2847
2850
  const {
2848
2851
  BINARY_TYPES,
2852
+ CLOSE_TIMEOUT,
2849
2853
  EMPTY_BUFFER,
2850
2854
  GUID,
2851
2855
  kForOnEventAttribute,
@@ -2860,7 +2864,6 @@ function requireWebsocket () {
2860
2864
  const { format, parse } = requireExtension();
2861
2865
  const { toBuffer } = requireBufferUtil();
2862
2866
 
2863
- const closeTimeout = 30 * 1000;
2864
2867
  const kAborted = Symbol('kAborted');
2865
2868
  const protocolVersions = [8, 13];
2866
2869
  const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
@@ -2916,6 +2919,7 @@ function requireWebsocket () {
2916
2919
  initAsClient(this, address, protocols, options);
2917
2920
  } else {
2918
2921
  this._autoPong = options.autoPong;
2922
+ this._closeTimeout = options.closeTimeout;
2919
2923
  this._isServer = true;
2920
2924
  }
2921
2925
  }
@@ -3457,6 +3461,8 @@ function requireWebsocket () {
3457
3461
  * times in the same tick
3458
3462
  * @param {Boolean} [options.autoPong=true] Specifies whether or not to
3459
3463
  * automatically send a pong in response to a ping
3464
+ * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait
3465
+ * for the closing handshake to finish after `websocket.close()` is called
3460
3466
  * @param {Function} [options.finishRequest] A function which can be used to
3461
3467
  * customize the headers of each http request before it is sent
3462
3468
  * @param {Boolean} [options.followRedirects=false] Whether or not to follow
@@ -3483,6 +3489,7 @@ function requireWebsocket () {
3483
3489
  const opts = {
3484
3490
  allowSynchronousEvents: true,
3485
3491
  autoPong: true,
3492
+ closeTimeout: CLOSE_TIMEOUT,
3486
3493
  protocolVersion: protocolVersions[1],
3487
3494
  maxPayload: 100 * 1024 * 1024,
3488
3495
  skipUTF8Validation: false,
@@ -3501,6 +3508,7 @@ function requireWebsocket () {
3501
3508
  };
3502
3509
 
3503
3510
  websocket._autoPong = opts.autoPong;
3511
+ websocket._closeTimeout = opts.closeTimeout;
3504
3512
 
3505
3513
  if (!protocolVersions.includes(opts.protocolVersion)) {
3506
3514
  throw new RangeError(
@@ -4118,7 +4126,7 @@ function requireWebsocket () {
4118
4126
  function setCloseTimer(websocket) {
4119
4127
  websocket._closeTimer = setTimeout(
4120
4128
  websocket._socket.destroy.bind(websocket._socket),
4121
- closeTimeout
4129
+ websocket._closeTimeout
4122
4130
  );
4123
4131
  }
4124
4132
 
@@ -4136,23 +4144,23 @@ function requireWebsocket () {
4136
4144
 
4137
4145
  websocket._readyState = WebSocket.CLOSING;
4138
4146
 
4139
- let chunk;
4140
-
4141
4147
  //
4142
4148
  // The close frame might not have been received or the `'end'` event emitted,
4143
4149
  // for example, if the socket was destroyed due to an error. Ensure that the
4144
4150
  // `receiver` stream is closed after writing any remaining buffered data to
4145
4151
  // it. If the readable side of the socket is in flowing mode then there is no
4146
- // buffered data as everything has been already written and `readable.read()`
4147
- // will return `null`. If instead, the socket is paused, any possible buffered
4148
- // data will be read as a single chunk.
4152
+ // buffered data as everything has been already written. If instead, the
4153
+ // socket is paused, any possible buffered data will be read as a single
4154
+ // chunk.
4149
4155
  //
4150
4156
  if (
4151
4157
  !this._readableState.endEmitted &&
4152
4158
  !websocket._closeFrameReceived &&
4153
4159
  !websocket._receiver._writableState.errorEmitted &&
4154
- (chunk = websocket._socket.read()) !== null
4160
+ this._readableState.length !== 0
4155
4161
  ) {
4162
+ const chunk = this.read(this._readableState.length);
4163
+
4156
4164
  websocket._receiver.write(chunk);
4157
4165
  }
4158
4166
 
@@ -4483,7 +4491,7 @@ function requireWebsocketServer () {
4483
4491
  const PerMessageDeflate = requirePermessageDeflate();
4484
4492
  const subprotocol = requireSubprotocol();
4485
4493
  const WebSocket = requireWebsocket();
4486
- const { GUID, kWebSocket } = requireConstants();
4494
+ const { CLOSE_TIMEOUT, GUID, kWebSocket } = requireConstants();
4487
4495
 
4488
4496
  const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
4489
4497
 
@@ -4510,6 +4518,9 @@ function requireWebsocketServer () {
4510
4518
  * pending connections
4511
4519
  * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
4512
4520
  * track clients
4521
+ * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
4522
+ * wait for the closing handshake to finish after `websocket.close()` is
4523
+ * called
4513
4524
  * @param {Function} [options.handleProtocols] A hook to handle protocols
4514
4525
  * @param {String} [options.host] The hostname where to bind the server
4515
4526
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
@@ -4539,6 +4550,7 @@ function requireWebsocketServer () {
4539
4550
  perMessageDeflate: false,
4540
4551
  handleProtocols: null,
4541
4552
  clientTracking: true,
4553
+ closeTimeout: CLOSE_TIMEOUT,
4542
4554
  verifyClient: null,
4543
4555
  noServer: false,
4544
4556
  backlog: null, // use default (511 as implemented in net.js)
@@ -5026,17 +5038,40 @@ function requireWebsocketServer () {
5026
5038
  var websocketServerExports = requireWebsocketServer();
5027
5039
  var WebSocketServer = /*@__PURE__*/getDefaultExportFromCjs(websocketServerExports);
5028
5040
 
5041
+ async function createViteServer(inlineConfig) {
5042
+ // Vite prints an error (https://github.com/vitejs/vite/issues/14328)
5043
+ // But Vitest works correctly either way
5044
+ const error = console.error;
5045
+ console.error = (...args) => {
5046
+ if (typeof args[0] === "string" && args[0].includes("WebSocket server error:")) return;
5047
+ error(...args);
5048
+ };
5049
+ const server = await createServer(inlineConfig);
5050
+ console.error = error;
5051
+ return server;
5052
+ }
5053
+ function isFileServingAllowed(configOrUrl, urlOrServer) {
5054
+ const config = typeof urlOrServer === "string" ? configOrUrl : urlOrServer.config;
5055
+ const url = typeof urlOrServer === "string" ? urlOrServer : configOrUrl;
5056
+ if (!config.server.fs.strict) return true;
5057
+ return isFileLoadingAllowed(config, fsPathFromUrl(url));
5058
+ }
5059
+ const FS_PREFIX = "/@fs/";
5060
+ const VOLUME_RE = /^[A-Z]:/i;
5061
+ function fsPathFromId(id) {
5062
+ const fsPath = normalizePath(id.startsWith(FS_PREFIX) ? id.slice(5) : id);
5063
+ return fsPath[0] === "/" || VOLUME_RE.test(fsPath) ? fsPath : `/${fsPath}`;
5064
+ }
5065
+ function fsPathFromUrl(url) {
5066
+ return fsPathFromId(cleanUrl(url));
5067
+ }
5068
+
5029
5069
  function getTestFileEnvironment(project, testFile, browser = false) {
5030
- let environment;
5031
- if (browser) environment = project.browser?.vite.environments.client;
5070
+ if (browser) return project.browser?.vite.environments.client;
5032
5071
  else for (const name in project.vite.environments) {
5033
5072
  const env = project.vite.environments[name];
5034
- if (env.moduleGraph.getModuleById(testFile)) {
5035
- environment = env;
5036
- break;
5037
- }
5073
+ if (env.moduleGraph.getModuleById(testFile)) return env;
5038
5074
  }
5039
- return environment;
5040
5075
  }
5041
5076
 
5042
5077
  async function getModuleGraph(ctx, projectName, testFilePath, browser = false) {
@@ -5044,7 +5079,7 @@ async function getModuleGraph(ctx, projectName, testFilePath, browser = false) {
5044
5079
  const externalized = /* @__PURE__ */ new Set();
5045
5080
  const inlined = /* @__PURE__ */ new Set();
5046
5081
  const project = ctx.getProjectByName(projectName);
5047
- const environment = getTestFileEnvironment(project, testFilePath, browser);
5082
+ const environment = project.config.experimental.viteModuleRunner === false ? project.vite.environments.__vitest__ : getTestFileEnvironment(project, testFilePath, browser);
5048
5083
  if (!environment) throw new Error(`Cannot find environment for ${testFilePath}`);
5049
5084
  const seen = /* @__PURE__ */ new Map();
5050
5085
  function get(mod) {
@@ -5175,7 +5210,7 @@ function setup(ctx, _server) {
5175
5210
  async getExternalResult(moduleId, testFileTaskId) {
5176
5211
  const testModule = ctx.state.getReportedEntityById(testFileTaskId);
5177
5212
  if (!testModule) return;
5178
- if (!isFileServingAllowed$1(testModule.project.vite.config, moduleId)) return;
5213
+ if (!isFileServingAllowed(testModule.project.vite.config, moduleId)) return;
5179
5214
  const result = {};
5180
5215
  try {
5181
5216
  result.source = await promises.readFile(moduleId, "utf-8");
@@ -5185,7 +5220,7 @@ function setup(ctx, _server) {
5185
5220
  async getTransformResult(projectName, moduleId, testFileTaskId, browser = false) {
5186
5221
  const project = ctx.getProjectByName(projectName);
5187
5222
  const testModule = ctx.state.getReportedEntityById(testFileTaskId);
5188
- if (!testModule || !isFileServingAllowed$1(project.vite.config, moduleId)) return;
5223
+ if (!testModule || !isFileServingAllowed(project.vite.config, moduleId)) return;
5189
5224
  const environment = getTestFileEnvironment(project, testModule.moduleId, browser);
5190
5225
  const moduleNode = environment?.moduleGraph.getModuleById(moduleId);
5191
5226
  if (!environment || !moduleNode?.transformResult) return;
@@ -5325,8 +5360,62 @@ function createDebugger(namespace) {
5325
5360
  if (debug.enabled) return debug;
5326
5361
  }
5327
5362
 
5363
+ async function getSpecificationsOptions(specifications) {
5364
+ const environments = /* @__PURE__ */ new WeakMap();
5365
+ const cache = /* @__PURE__ */ new Map();
5366
+ const tags = /* @__PURE__ */ new WeakMap();
5367
+ await Promise.all(specifications.map(async (spec) => {
5368
+ const { moduleId: filepath, project, pool } = spec;
5369
+ // browser pool handles its own environment
5370
+ if (pool === "browser") return;
5371
+ // reuse if projects have the same test files
5372
+ let code = cache.get(filepath);
5373
+ if (!code) {
5374
+ code = await promises.readFile(filepath, "utf-8").catch(() => "");
5375
+ cache.set(filepath, code);
5376
+ }
5377
+ const { env = project.config.environment || "node", envOptions, tags: specTags = [] } = detectCodeBlock(code);
5378
+ tags.set(spec, specTags);
5379
+ const environment = {
5380
+ name: env,
5381
+ options: envOptions ? { [env === "happy-dom" ? "happyDOM" : env]: envOptions } : null
5382
+ };
5383
+ environments.set(spec, environment);
5384
+ }));
5385
+ return {
5386
+ environments,
5387
+ tags
5388
+ };
5389
+ }
5390
+ function detectCodeBlock(content) {
5391
+ const env = content.match(/@(?:vitest|jest)-environment\s+([\w-]+)\b/)?.[1];
5392
+ let envOptionsJson = content.match(/@(?:vitest|jest)-environment-options\s+(.+)/)?.[1];
5393
+ if (envOptionsJson?.endsWith("*/"))
5394
+ // Trim closing Docblock characters the above regex might have captured
5395
+ envOptionsJson = envOptionsJson.slice(0, -2);
5396
+ const envOptions = JSON.parse(envOptionsJson || "null");
5397
+ const tags = [];
5398
+ let tagMatch;
5399
+ // eslint-disable-next-line no-cond-assign
5400
+ while (tagMatch = content.match(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/)) {
5401
+ tags.push(tagMatch[2]);
5402
+ content = content.slice(tagMatch.index + tagMatch[0].length);
5403
+ }
5404
+ return {
5405
+ env,
5406
+ envOptions,
5407
+ tags
5408
+ };
5409
+ }
5410
+
5328
5411
  const debug$1 = createDebugger("vitest:ast-collect-info");
5329
5412
  const verbose = createDebugger("vitest:ast-collect-verbose");
5413
+ function isTestFunctionName(name) {
5414
+ return name === "it" || name === "test" || name.startsWith("test") || name.endsWith("Test");
5415
+ }
5416
+ function isVitestFunctionName(name) {
5417
+ return name === "describe" || name === "suite" || isTestFunctionName(name);
5418
+ }
5330
5419
  function astParseFile(filepath, code) {
5331
5420
  const ast = parseAst(code);
5332
5421
  if (verbose) verbose("Collecting", filepath, code);
@@ -5338,12 +5427,7 @@ function astParseFile(filepath, code) {
5338
5427
  if (callee.type === "CallExpression") return getName(callee.callee);
5339
5428
  if (callee.type === "TaggedTemplateExpression") return getName(callee.tag);
5340
5429
  if (callee.type === "MemberExpression") {
5341
- if (callee.object?.type === "Identifier" && [
5342
- "it",
5343
- "test",
5344
- "describe",
5345
- "suite"
5346
- ].includes(callee.object.name)) return callee.object?.name;
5430
+ if (callee.object?.type === "Identifier" && isVitestFunctionName(callee.object.name)) return callee.object?.name;
5347
5431
  if (callee.object?.name?.startsWith("__vite_ssr_") || callee.object?.object?.name?.startsWith("__vite_ssr_") && callee.object?.property?.name === "Vitest") return getName(callee.property);
5348
5432
  // call as `__vite_ssr__.test.skip()`
5349
5433
  return getName(callee.object?.property);
@@ -5359,12 +5443,7 @@ function astParseFile(filepath, code) {
5359
5443
  const { callee } = node;
5360
5444
  const name = getName(callee);
5361
5445
  if (!name) return;
5362
- if (![
5363
- "it",
5364
- "test",
5365
- "describe",
5366
- "suite"
5367
- ].includes(name)) {
5446
+ if (!isVitestFunctionName(name)) {
5368
5447
  verbose?.(`Skipping ${name} (unknown call)`);
5369
5448
  return;
5370
5449
  }
@@ -5375,7 +5454,9 @@ function astParseFile(filepath, code) {
5375
5454
  "each",
5376
5455
  "for",
5377
5456
  "skipIf",
5378
- "runIf"
5457
+ "runIf",
5458
+ "extend",
5459
+ "scoped"
5379
5460
  ].includes(mode)) return;
5380
5461
  let start;
5381
5462
  const end = node.end;
@@ -5400,15 +5481,32 @@ function astParseFile(filepath, code) {
5400
5481
  const property = callee.tag?.property?.name;
5401
5482
  isDynamicEach = property === "each" || property === "for";
5402
5483
  }
5403
- debug$1?.("Found", name, message, `(${mode})`);
5484
+ // Extract tags from the second argument if it's an options object
5485
+ const tags = [];
5486
+ const secondArg = node.arguments?.[1];
5487
+ if (secondArg?.type === "ObjectExpression") {
5488
+ const tagsProperty = secondArg.properties?.find((p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "tags");
5489
+ if (tagsProperty) {
5490
+ const tagsValue = tagsProperty.value;
5491
+ if (tagsValue?.type === "Literal" && typeof tagsValue.value === "string")
5492
+ // tags: 'single-tag'
5493
+ tags.push(tagsValue.value);
5494
+ else if (tagsValue?.type === "ArrayExpression") {
5495
+ // tags: ['tag1', 'tag2']
5496
+ for (const element of tagsValue.elements || []) if (element?.type === "Literal" && typeof element.value === "string") tags.push(element.value);
5497
+ }
5498
+ }
5499
+ }
5500
+ debug$1?.("Found", name, message, `(${mode})`, tags.length ? `[${tags.join(", ")}]` : "");
5404
5501
  definitions.push({
5405
5502
  start,
5406
5503
  end,
5407
5504
  name: message,
5408
- type: name === "it" || name === "test" ? "test" : "suite",
5505
+ type: isTestFunctionName(name) ? "test" : "suite",
5409
5506
  mode,
5410
5507
  task: null,
5411
- dynamic: isDynamicEach
5508
+ dynamic: isDynamicEach,
5509
+ tags
5412
5510
  });
5413
5511
  } });
5414
5512
  return {
@@ -5454,22 +5552,23 @@ function serializeError(ctx, error) {
5454
5552
  message: error.message
5455
5553
  }];
5456
5554
  }
5457
- function createFileTask(testFilepath, code, requestMap, options) {
5555
+ function createFileTask(testFilepath, code, requestMap, config, filepath, fileTags) {
5458
5556
  const { definitions, ast } = astParseFile(testFilepath, code);
5459
5557
  const file = {
5460
- filepath: options.filepath,
5558
+ filepath,
5461
5559
  type: "suite",
5462
- id: /* @__PURE__ */ generateHash$1(`${testFilepath}${options.name || ""}`),
5560
+ id: /* @__PURE__ */ generateHash$1(`${testFilepath}${config.name || ""}`),
5463
5561
  name: testFilepath,
5464
5562
  fullName: testFilepath,
5465
5563
  mode: "run",
5466
5564
  tasks: [],
5467
5565
  start: ast.start,
5468
5566
  end: ast.end,
5469
- projectName: options.name,
5567
+ projectName: config.name,
5470
5568
  meta: {},
5471
5569
  pool: "browser",
5472
- file: null
5570
+ file: null,
5571
+ tags: fileTags || []
5473
5572
  };
5474
5573
  file.file = file;
5475
5574
  const indexMap = createIndexLocationsMap(code);
@@ -5494,9 +5593,13 @@ function createFileTask(testFilepath, code, requestMap, options) {
5494
5593
  });
5495
5594
  if (originalLocation.column != null) {
5496
5595
  verbose?.(`Found location for`, definition.type, definition.name, `${processedLocation.line}:${processedLocation.column}`, "->", `${originalLocation.line}:${originalLocation.column}`);
5497
- location = originalLocation;
5596
+ location = {
5597
+ line: originalLocation.line,
5598
+ column: originalLocation.column
5599
+ };
5498
5600
  } else debug$1?.("Cannot find original location for", definition.type, definition.name, `${processedLocation.column}:${processedLocation.line}`);
5499
5601
  } else debug$1?.("Cannot find original location for", definition.type, definition.name, `${definition.start}`);
5602
+ const taskTags = unique([...latestSuite.tags || [], ...definition.tags]);
5500
5603
  if (definition.type === "suite") {
5501
5604
  const task = {
5502
5605
  type: definition.type,
@@ -5505,6 +5608,7 @@ function createFileTask(testFilepath, code, requestMap, options) {
5505
5608
  file,
5506
5609
  tasks: [],
5507
5610
  mode,
5611
+ each: definition.dynamic,
5508
5612
  name: definition.name,
5509
5613
  fullName: createTaskName([latestSuite.fullName, definition.name]),
5510
5614
  fullTestName: createTaskName([latestSuite.fullTestName, definition.name]),
@@ -5512,18 +5616,21 @@ function createFileTask(testFilepath, code, requestMap, options) {
5512
5616
  start: definition.start,
5513
5617
  location,
5514
5618
  dynamic: definition.dynamic,
5515
- meta: {}
5619
+ meta: {},
5620
+ tags: taskTags
5516
5621
  };
5517
5622
  definition.task = task;
5518
5623
  latestSuite.tasks.push(task);
5519
5624
  lastSuite = task;
5520
5625
  return;
5521
5626
  }
5627
+ validateTags(config, taskTags);
5522
5628
  const task = {
5523
5629
  type: definition.type,
5524
5630
  id: "",
5525
5631
  suite: latestSuite,
5526
5632
  file,
5633
+ each: definition.dynamic,
5527
5634
  mode,
5528
5635
  context: {},
5529
5636
  name: definition.name,
@@ -5536,20 +5643,21 @@ function createFileTask(testFilepath, code, requestMap, options) {
5536
5643
  meta: {},
5537
5644
  timeout: 0,
5538
5645
  annotations: [],
5539
- artifacts: []
5646
+ artifacts: [],
5647
+ tags: taskTags
5540
5648
  };
5541
5649
  definition.task = task;
5542
5650
  latestSuite.tasks.push(task);
5543
5651
  });
5544
5652
  calculateSuiteHash(file);
5545
5653
  const hasOnly = someTasksAreOnly(file);
5546
- interpretTaskModes(file, options.testNamePattern, void 0, hasOnly, false, options.allowOnly);
5654
+ interpretTaskModes(file, config.testNamePattern, void 0, void 0, void 0, hasOnly, false, config.allowOnly);
5547
5655
  markDynamicTests(file.tasks);
5548
5656
  if (!file.tasks.length) file.result = {
5549
5657
  state: "fail",
5550
5658
  errors: [{
5551
5659
  name: "Error",
5552
- message: `No test suite found in file ${options.filepath}`
5660
+ message: `No test suite found in file ${filepath}`
5553
5661
  }]
5554
5662
  };
5555
5663
  return file;
@@ -5561,18 +5669,17 @@ async function astCollectTests(project, filepath) {
5561
5669
  debug$1?.("Cannot parse", testFilepath, "(vite didn't return anything)");
5562
5670
  return createFailedFileTask(project, filepath, /* @__PURE__ */ new Error(`Failed to parse ${testFilepath}. Vite didn't return anything.`));
5563
5671
  }
5564
- return createFileTask(testFilepath, request.code, request.map, {
5565
- name: project.config.name,
5566
- filepath,
5567
- allowOnly: project.config.allowOnly,
5568
- testNamePattern: project.config.testNamePattern,
5569
- pool: project.browser ? "browser" : project.config.pool
5570
- });
5672
+ return createFileTask(testFilepath, request.code, request.map, project.serializedConfig, filepath, request.fileTags);
5571
5673
  }
5572
5674
  async function transformSSR(project, filepath) {
5573
- const request = await project.vite.transformRequest(filepath, { ssr: false });
5574
- if (!request) return null;
5575
- return await project.vite.ssrTransform(request.code, request.map, filepath);
5675
+ const { env: pragmaEnv, tags: fileTags } = detectCodeBlock(await promises.readFile(filepath, "utf-8").catch(() => ""));
5676
+ // Use environment from pragma if defined, otherwise fall back to config
5677
+ const environment = pragmaEnv || project.config.environment;
5678
+ const transformResult = await (environment === "jsdom" || environment === "happy-dom" ? project.vite.environments.client : project.vite.environments.ssr).transformRequest(filepath);
5679
+ return transformResult ? {
5680
+ ...transformResult,
5681
+ fileTags
5682
+ } : null;
5576
5683
  }
5577
5684
  function markDynamicTests(tasks) {
5578
5685
  for (const task of tasks) {
@@ -6560,6 +6667,25 @@ class Logger {
6560
6667
  this._highlights.set(filename, code);
6561
6668
  return code;
6562
6669
  }
6670
+ printNoTestTagsFound() {
6671
+ this.error(c.bgRed(" ERROR "), c.red("No test tags found in any project. Exiting with code 1."));
6672
+ }
6673
+ printTags() {
6674
+ const vitest = this.ctx;
6675
+ const rootProject = vitest.getRootProject();
6676
+ const projects = [rootProject, ...vitest.projects.filter((p) => p !== rootProject)];
6677
+ if (!projects.some((p) => p.config.tags && p.config.tags.length > 0)) {
6678
+ process.exitCode = 1;
6679
+ return this.printNoTestTagsFound();
6680
+ }
6681
+ for (const project of projects) {
6682
+ if (project.name) this.log(formatProjectName(project, ""));
6683
+ project.config.tags.forEach((tag) => {
6684
+ const tagLog = `${tag.name}${tag.description ? `: ${tag.description}` : ""}`;
6685
+ this.log(` ${tagLog}`);
6686
+ });
6687
+ }
6688
+ }
6563
6689
  printNoTestFound(filters) {
6564
6690
  const config = this.ctx.config;
6565
6691
  if (config.watch && (config.changed || config.related?.length)) this.log(`No affected ${config.mode} files found\n`);
@@ -6992,35 +7118,6 @@ function stringToBytes(input, percentageReference) {
6992
7118
  return null;
6993
7119
  }
6994
7120
 
6995
- async function getSpecificationsEnvironments(specifications) {
6996
- const environments = /* @__PURE__ */ new WeakMap();
6997
- const cache = /* @__PURE__ */ new Map();
6998
- await Promise.all(specifications.map(async (spec) => {
6999
- const { moduleId: filepath, project } = spec;
7000
- // reuse if projects have the same test files
7001
- let code = cache.get(filepath);
7002
- if (!code) {
7003
- code = await promises.readFile(filepath, "utf-8");
7004
- cache.set(filepath, code);
7005
- }
7006
- // 1. Check for control comments in the file
7007
- let env = code.match(/@(?:vitest|jest)-environment\s+([\w-]+)\b/)?.[1];
7008
- // 2. Fallback to global env
7009
- env ||= project.config.environment || "node";
7010
- let envOptionsJson = code.match(/@(?:vitest|jest)-environment-options\s+(.+)/)?.[1];
7011
- if (envOptionsJson?.endsWith("*/"))
7012
- // Trim closing Docblock characters the above regex might have captured
7013
- envOptionsJson = envOptionsJson.slice(0, -2);
7014
- const envOptions = JSON.parse(envOptionsJson || "null");
7015
- const environment = {
7016
- name: env,
7017
- options: envOptions ? { [env === "happy-dom" ? "happyDOM" : env]: envOptions } : null
7018
- };
7019
- environments.set(spec, environment);
7020
- }));
7021
- return environments;
7022
- }
7023
-
7024
7121
  const debug = createDebugger("vitest:browser:pool");
7025
7122
  function createBrowserPool(vitest) {
7026
7123
  const providers = /* @__PURE__ */ new Set();
@@ -7048,11 +7145,29 @@ function createBrowserPool(vitest) {
7048
7145
  };
7049
7146
  const runWorkspaceTests = async (method, specs) => {
7050
7147
  const groupedFiles = /* @__PURE__ */ new Map();
7051
- for (const { project, moduleId, testLines } of specs) {
7148
+ const testFilesCode = /* @__PURE__ */ new Map();
7149
+ const testFileTags = /* @__PURE__ */ new WeakMap();
7150
+ await Promise.all(specs.map(async (spec) => {
7151
+ let code = testFilesCode.get(spec.moduleId);
7152
+ // TODO: this really should be done only once when collecting specifications
7153
+ if (code == null) {
7154
+ code = await readFile(spec.moduleId, "utf-8").catch(() => "");
7155
+ testFilesCode.set(spec.moduleId, code);
7156
+ }
7157
+ const { tags } = detectCodeBlock(code);
7158
+ testFileTags.set(spec, tags);
7159
+ }));
7160
+ // to keep the sorting, we need to iterate over specs separately
7161
+ for (const spec of specs) {
7162
+ const { project, moduleId, testLines, testIds, testNamePattern, testTagsFilter } = spec;
7052
7163
  const files = groupedFiles.get(project) || [];
7053
7164
  files.push({
7054
7165
  filepath: moduleId,
7055
- testLocations: testLines
7166
+ testLocations: testLines,
7167
+ testIds,
7168
+ testNamePattern,
7169
+ testTagsFilter,
7170
+ fileTags: testFileTags.get(spec)
7056
7171
  });
7057
7172
  groupedFiles.set(project, files);
7058
7173
  }
@@ -7176,7 +7291,7 @@ class BrowserPool {
7176
7291
  let page = this._traces.$(`vitest.browser.open`, {
7177
7292
  context: this._otel.context,
7178
7293
  attributes: { "vitest.browser.session_id": sessionId }
7179
- }, () => this.openPage(sessionId));
7294
+ }, () => this.openPage(sessionId, { parallel: workerCount > 1 }));
7180
7295
  page = page.then(() => {
7181
7296
  // start running tests on the page when it's ready
7182
7297
  this.runNextTest(method, sessionId);
@@ -7187,14 +7302,14 @@ class BrowserPool {
7187
7302
  debug?.("all sessions are created");
7188
7303
  return this._promise;
7189
7304
  }
7190
- async openPage(sessionId) {
7305
+ async openPage(sessionId, options) {
7191
7306
  const sessionPromise = this.project.vitest._browserSessions.createSession(sessionId, this.project, this);
7192
7307
  const browser = this.project.browser;
7193
7308
  const url = new URL("/__vitest_test__/", this.options.origin);
7194
7309
  url.searchParams.set("sessionId", sessionId);
7195
7310
  const otelCarrier = this._traces.getContextCarrier();
7196
7311
  if (otelCarrier) url.searchParams.set("otelCarrier", JSON.stringify(otelCarrier));
7197
- const pagePromise = browser.provider.openPage(sessionId, url.toString());
7312
+ const pagePromise = browser.provider.openPage(sessionId, url.toString(), options);
7198
7313
  await Promise.all([sessionPromise, pagePromise]);
7199
7314
  }
7200
7315
  getOrchestrator(sessionId) {
@@ -7364,6 +7479,34 @@ function createMethodsRPC(project, methodsOptions = {}) {
7364
7479
  },
7365
7480
  getCountOfFailedTests() {
7366
7481
  return vitest.state.getCountOfFailedTests();
7482
+ },
7483
+ ensureModuleGraphEntry(id, importer) {
7484
+ const filepath = id.startsWith("file:") ? fileURLToPath(id) : id;
7485
+ const importerPath = importer.startsWith("file:") ? fileURLToPath(importer) : importer;
7486
+ // environment itself doesn't matter
7487
+ const moduleGraph = project.vite.environments.__vitest__?.moduleGraph;
7488
+ if (!moduleGraph) {
7489
+ // TODO: is it possible?
7490
+ console.error("no module graph for", id);
7491
+ return;
7492
+ }
7493
+ const importerNode = moduleGraph.getModuleById(importerPath) || moduleGraph.createFileOnlyEntry(importerPath);
7494
+ const moduleNode = moduleGraph.getModuleById(filepath) || moduleGraph.createFileOnlyEntry(filepath);
7495
+ if (!moduleGraph.idToModuleMap.has(importerPath)) {
7496
+ importerNode.id = importerPath;
7497
+ moduleGraph.idToModuleMap.set(importerPath, importerNode);
7498
+ }
7499
+ if (!moduleGraph.idToModuleMap.has(filepath)) {
7500
+ moduleNode.id = filepath;
7501
+ moduleGraph.idToModuleMap.set(filepath, moduleNode);
7502
+ }
7503
+ // this is checked by the "printError" function - TODO: is there a better way?
7504
+ moduleNode.transformResult = {
7505
+ code: " ",
7506
+ map: null
7507
+ };
7508
+ importerNode.importedModules.add(moduleNode);
7509
+ moduleNode.importers.add(importerNode);
7367
7510
  }
7368
7511
  };
7369
7512
  }
@@ -8205,7 +8348,7 @@ function createPool(ctx) {
8205
8348
  const taskGroups = [];
8206
8349
  let workerId = 0;
8207
8350
  const sorted = await sequencer.sort(specs);
8208
- const environments = await getSpecificationsEnvironments(specs);
8351
+ const { environments, tags } = await getSpecificationsOptions(specs);
8209
8352
  const groups = groupSpecs(sorted, environments);
8210
8353
  const projectEnvs = /* @__PURE__ */ new WeakMap();
8211
8354
  const projectExecArgvs = /* @__PURE__ */ new WeakMap();
@@ -8247,7 +8390,11 @@ function createPool(ctx) {
8247
8390
  context: {
8248
8391
  files: specs.map((spec) => ({
8249
8392
  filepath: spec.moduleId,
8250
- testLocations: spec.testLines
8393
+ fileTags: tags.get(spec),
8394
+ testLocations: spec.testLines,
8395
+ testNamePattern: spec.testNamePattern,
8396
+ testIds: spec.testIds,
8397
+ testTagsFilter: spec.testTagsFilter
8251
8398
  })),
8252
8399
  invalidates,
8253
8400
  providedContext: project.getProvidedContext(),
@@ -8521,8 +8668,13 @@ function serializeConfig(project) {
8521
8668
  experimental: {
8522
8669
  fsModuleCache: config.experimental.fsModuleCache ?? false,
8523
8670
  printImportBreakdown: config.experimental.printImportBreakdown,
8671
+ viteModuleRunner: config.experimental.viteModuleRunner ?? true,
8672
+ nodeLoader: config.experimental.nodeLoader ?? true,
8524
8673
  openTelemetry: config.experimental.openTelemetry
8525
- }
8674
+ },
8675
+ tags: config.tags || [],
8676
+ tagsFilter: config.tagsFilter,
8677
+ strictTags: config.strictTags ?? true
8526
8678
  };
8527
8679
  }
8528
8680
 
@@ -9580,15 +9732,12 @@ function WorkspaceVitestPlugin(project, options) {
9580
9732
  label: name,
9581
9733
  color
9582
9734
  } };
9735
+ vitestConfig.experimental ??= {};
9583
9736
  // always inherit the global `fsModuleCache` value even without `extends: true`
9584
- if (testConfig.experimental?.fsModuleCache == null && project.vitest.config.experimental?.fsModuleCache !== null) {
9585
- vitestConfig.experimental ??= {};
9586
- vitestConfig.experimental.fsModuleCache = project.vitest.config.experimental.fsModuleCache;
9587
- }
9588
- if (testConfig.experimental?.fsModuleCachePath == null && project.vitest.config.experimental?.fsModuleCachePath !== null) {
9589
- vitestConfig.experimental ??= {};
9590
- vitestConfig.experimental.fsModuleCachePath = project.vitest.config.experimental.fsModuleCachePath;
9591
- }
9737
+ if (testConfig.experimental?.fsModuleCache == null && project.vitest.config.experimental?.fsModuleCache != null) vitestConfig.experimental.fsModuleCache = project.vitest.config.experimental.fsModuleCache;
9738
+ if (testConfig.experimental?.fsModuleCachePath == null && project.vitest.config.experimental?.fsModuleCachePath != null) vitestConfig.experimental.fsModuleCachePath = project.vitest.config.experimental.fsModuleCachePath;
9739
+ if (testConfig.experimental?.viteModuleRunner == null && project.vitest.config.experimental?.viteModuleRunner != null) vitestConfig.experimental.viteModuleRunner = project.vitest.config.experimental.viteModuleRunner;
9740
+ if (testConfig.experimental?.nodeLoader == null && project.vitest.config.experimental?.nodeLoader != null) vitestConfig.experimental.nodeLoader = project.vitest.config.experimental.nodeLoader;
9592
9741
  return {
9593
9742
  base: "/",
9594
9743
  environments: { __vitest__: { dev: {} } },
@@ -9807,7 +9956,7 @@ function matchPattern(id, moduleDirectories, patterns) {
9807
9956
 
9808
9957
  class TestSpecification {
9809
9958
  /**
9810
- * The task ID associated with the test module.
9959
+ * The task id associated with the test module.
9811
9960
  */
9812
9961
  taskId;
9813
9962
  /**
@@ -9815,29 +9964,51 @@ class TestSpecification {
9815
9964
  */
9816
9965
  project;
9817
9966
  /**
9818
- * The ID of the module in the Vite module graph. It is usually an absolute file path.
9967
+ * The id of the module in the Vite module graph. It is usually an absolute file path.
9819
9968
  */
9820
9969
  moduleId;
9821
9970
  /**
9822
- * The current test pool. It's possible to have multiple pools in a single test project with `poolMatchGlob` and `typecheck.enabled`.
9823
- * @experimental In Vitest 4, the project will only support a single pool and this property will be removed.
9971
+ * The current test pool. It's possible to have multiple pools in a single test project with `typecheck.enabled`.
9972
+ * @experimental In later versions, the project will only support a single pool.
9824
9973
  */
9825
9974
  pool;
9826
9975
  /**
9827
9976
  * Line numbers of the test locations to run.
9828
9977
  */
9829
9978
  testLines;
9830
- constructor(project, moduleId, pool, testLines) {
9831
- const name = project.config.name;
9832
- const hashName = pool !== "typescript" ? name : name ? `${name}:__typecheck__` : "__typecheck__";
9979
+ /**
9980
+ * Regular expression pattern to filter test names.
9981
+ */
9982
+ testNamePattern;
9983
+ /**
9984
+ * The ids of tasks inside of this specification to run.
9985
+ */
9986
+ testIds;
9987
+ /**
9988
+ * The tags of tests to run.
9989
+ */
9990
+ testTagsFilter;
9991
+ /**
9992
+ * This class represents a test suite for a test module within a single project.
9993
+ * @internal
9994
+ */
9995
+ constructor(project, moduleId, pool, testLinesOrOptions) {
9996
+ const projectName = project.config.name;
9997
+ const hashName = pool !== "typescript" ? projectName : projectName ? `${projectName}:__typecheck__` : "__typecheck__";
9833
9998
  this.taskId = generateFileHash(relative(project.config.root, moduleId), hashName);
9834
9999
  this.project = project;
9835
10000
  this.moduleId = moduleId;
9836
10001
  this.pool = pool;
9837
- this.testLines = testLines;
10002
+ if (Array.isArray(testLinesOrOptions)) this.testLines = testLinesOrOptions;
10003
+ else if (testLinesOrOptions && typeof testLinesOrOptions === "object") {
10004
+ this.testLines = testLinesOrOptions.testLines;
10005
+ this.testNamePattern = testLinesOrOptions.testNamePattern;
10006
+ this.testIds = testLinesOrOptions.testIds;
10007
+ this.testTagsFilter = testLinesOrOptions.testTagsFilter;
10008
+ }
9838
10009
  }
9839
10010
  /**
9840
- * Test module associated with the specification.
10011
+ * Test module associated with the specification. This will be `undefined` if tests have not been run yet.
9841
10012
  */
9842
10013
  get testModule() {
9843
10014
  const task = this.project.vitest.state.idMap.get(this.taskId);
@@ -9853,40 +10024,15 @@ class TestSpecification {
9853
10024
  this.moduleId,
9854
10025
  {
9855
10026
  pool: this.pool,
9856
- testLines: this.testLines
10027
+ testLines: this.testLines,
10028
+ testIds: this.testIds,
10029
+ testNamePattern: this.testNamePattern,
10030
+ testTagsFilter: this.testTagsFilter
9857
10031
  }
9858
10032
  ];
9859
10033
  }
9860
10034
  }
9861
10035
 
9862
- async function createViteServer(inlineConfig) {
9863
- // Vite prints an error (https://github.com/vitejs/vite/issues/14328)
9864
- // But Vitest works correctly either way
9865
- const error = console.error;
9866
- console.error = (...args) => {
9867
- if (typeof args[0] === "string" && args[0].includes("WebSocket server error:")) return;
9868
- error(...args);
9869
- };
9870
- const server = await createServer(inlineConfig);
9871
- console.error = error;
9872
- return server;
9873
- }
9874
- function isFileServingAllowed(configOrUrl, urlOrServer) {
9875
- const config = typeof urlOrServer === "string" ? configOrUrl : urlOrServer.config;
9876
- const url = typeof urlOrServer === "string" ? urlOrServer : configOrUrl;
9877
- if (!config.server.fs.strict) return true;
9878
- return isFileLoadingAllowed(config, fsPathFromUrl(url));
9879
- }
9880
- const FS_PREFIX = "/@fs/";
9881
- const VOLUME_RE = /^[A-Z]:/i;
9882
- function fsPathFromId(id) {
9883
- const fsPath = normalizePath(id.startsWith(FS_PREFIX) ? id.slice(5) : id);
9884
- return fsPath[0] === "/" || VOLUME_RE.test(fsPath) ? fsPath : `/${fsPath}`;
9885
- }
9886
- function fsPathFromUrl(url) {
9887
- return fsPathFromId(cleanUrl(url));
9888
- }
9889
-
9890
10036
  class TestProject {
9891
10037
  /**
9892
10038
  * The global Vitest instance.
@@ -9962,8 +10108,8 @@ class TestProject {
9962
10108
  * Creates a new test specification. Specifications describe how to run tests.
9963
10109
  * @param moduleId The file path
9964
10110
  */
9965
- createSpecification(moduleId, locations, pool) {
9966
- return new TestSpecification(this, moduleId, pool || getFilePoolName(this), locations);
10111
+ createSpecification(moduleId, locationsOrOptions, pool) {
10112
+ return new TestSpecification(this, moduleId, pool || getFilePoolName(this), locationsOrOptions);
9967
10113
  }
9968
10114
  toJSON() {
9969
10115
  return {
@@ -10188,7 +10334,7 @@ class TestProject {
10188
10334
  if (!this.closingPromise) this.closingPromise = Promise.all([
10189
10335
  this.vite?.close(),
10190
10336
  this.typechecker?.stop(),
10191
- this.browser?.close(),
10337
+ (this.browser || this._parent?._parentBrowser?.vite)?.close(),
10192
10338
  this.clearTmpDir()
10193
10339
  ].filter(Boolean)).then(() => {
10194
10340
  if (!this.runner.isClosed()) return this.runner.close();
@@ -10227,7 +10373,11 @@ class TestProject {
10227
10373
  this._serializedDefines = createDefinesScript(server.config.define);
10228
10374
  this._fetcher = createFetchModuleFunction(this._resolver, this._config, this.vitest._fsCache, this.vitest._traces, this.tmpDir);
10229
10375
  const environment = server.environments.__vitest__;
10230
- this.runner = new ServerModuleRunner(environment, this._fetcher, this._config);
10376
+ this.runner = this._config.experimental.viteModuleRunner === false ? new NativeModuleRunner(this._config.root) : new ServerModuleRunner(environment, this._fetcher, this._config);
10377
+ }
10378
+ /** @internal */
10379
+ _getViteEnvironments() {
10380
+ return [...Object.values(this.browser?.vite.environments || {}), ...Object.values(this.vite.environments || {})];
10231
10381
  }
10232
10382
  _serializeOverriddenConfig() {
10233
10383
  // TODO: serialize the config _once_ or when needed
@@ -10342,7 +10492,8 @@ async function resolveProjects(vitest, cliOptions, workspaceConfigPath, projects
10342
10492
  "printConsoleTrace",
10343
10493
  "inspect",
10344
10494
  "inspectBrk",
10345
- "fileParallelism"
10495
+ "fileParallelism",
10496
+ "tagsFilter"
10346
10497
  ].reduce((acc, name) => {
10347
10498
  if (name in cliOptions) acc[name] = cliOptions[name];
10348
10499
  return acc;
@@ -10361,6 +10512,14 @@ async function resolveProjects(vitest, cliOptions, workspaceConfigPath, projects
10361
10512
  ...options,
10362
10513
  root,
10363
10514
  configFile,
10515
+ plugins: [{
10516
+ name: "vitest:tags",
10517
+ configResolved(config) {
10518
+ config.test ??= {};
10519
+ config.test.tags = options.test?.tags;
10520
+ },
10521
+ api: { vitest: { experimental: { ignoreFsModuleCache: true } } }
10522
+ }, ...options.plugins || []],
10364
10523
  test: {
10365
10524
  ...options.test,
10366
10525
  ...cliOverrides
@@ -10827,6 +10986,10 @@ class TestCase extends ReportedTaskImplementation {
10827
10986
  * Parent suite. If the test was called directly inside the module, the parent will be the module itself.
10828
10987
  */
10829
10988
  parent;
10989
+ /**
10990
+ * Tags associated with the test.
10991
+ */
10992
+ tags;
10830
10993
  /** @internal */
10831
10994
  constructor(task, project) {
10832
10995
  super(task, project);
@@ -10836,6 +10999,7 @@ class TestCase extends ReportedTaskImplementation {
10836
10999
  if (suite) this.parent = getReportedTask(project, suite);
10837
11000
  else this.parent = this.module;
10838
11001
  this.options = buildOptions(task);
11002
+ this.tags = this.options.tags || [];
10839
11003
  }
10840
11004
  /**
10841
11005
  * Full name of the test including all parent suites separated with `>`.
@@ -10912,6 +11076,13 @@ class TestCase extends ReportedTaskImplementation {
10912
11076
  flaky: !!result.retryCount && result.state === "pass" && result.retryCount > 0
10913
11077
  };
10914
11078
  }
11079
+ /**
11080
+ * Returns a new test specification that can be used to filter or run this specific test case.
11081
+ */
11082
+ toTestSpecification() {
11083
+ const isTypecheck = this.task.meta.typecheck === true;
11084
+ return this.project.createSpecification(this.module.moduleId, { testIds: [this.id] }, isTypecheck ? "typecheck" : void 0);
11085
+ }
10915
11086
  }
10916
11087
  class TestCollection {
10917
11088
  #task;
@@ -11031,6 +11202,14 @@ class TestSuite extends SuiteImplementation {
11031
11202
  return getSuiteState(this.task);
11032
11203
  }
11033
11204
  /**
11205
+ * Returns a new test specification that can be used to filter or run this specific test suite.
11206
+ */
11207
+ toTestSpecification() {
11208
+ const isTypecheck = this.task.meta.typecheck === true;
11209
+ const testIds = [...this.children.allTests()].map((test) => test.id);
11210
+ return this.project.createSpecification(this.module.moduleId, { testIds }, isTypecheck ? "typecheck" : void 0);
11211
+ }
11212
+ /**
11034
11213
  * Full name of the suite including all parent suites separated with `>`.
11035
11214
  */
11036
11215
  get fullName() {
@@ -11066,6 +11245,13 @@ class TestModule extends SuiteImplementation {
11066
11245
  else if (typeof task.viteEnvironment === "string") this.viteEnvironment = project.vite.environments[task.viteEnvironment];
11067
11246
  }
11068
11247
  /**
11248
+ * Returns a new test specification that can be used to filter or run this specific test module.
11249
+ */
11250
+ toTestSpecification() {
11251
+ const isTypecheck = this.task.meta.typecheck === true;
11252
+ return this.project.createSpecification(this.moduleId, void 0, isTypecheck ? "typecheck" : void 0);
11253
+ }
11254
+ /**
11069
11255
  * Checks the running state of the test file.
11070
11256
  */
11071
11257
  state() {
@@ -11099,6 +11285,8 @@ function buildOptions(task) {
11099
11285
  shuffle: task.shuffle,
11100
11286
  retry: task.retry,
11101
11287
  repeats: task.repeats,
11288
+ tags: task.tags,
11289
+ timeout: task.type === "test" ? task.timeout : void 0,
11102
11290
  mode: task.mode
11103
11291
  };
11104
11292
  }
@@ -11279,6 +11467,19 @@ class StateManager {
11279
11467
  }
11280
11468
  }
11281
11469
 
11470
+ function populateProjectsTags(rootProject, projects) {
11471
+ // Include root project if not already in the list
11472
+ const allProjects = projects.includes(rootProject) ? projects : [rootProject, ...projects];
11473
+ // Collect all tags from all projects (first definition wins)
11474
+ const globalTags = /* @__PURE__ */ new Map();
11475
+ for (const project of allProjects) for (const tag of project.config.tags || []) if (!globalTags.has(tag.name)) globalTags.set(tag.name, tag);
11476
+ // Add missing tags to each project (without overriding local definitions)
11477
+ for (const project of allProjects) {
11478
+ const projectTagNames = new Set(project.config.tags.map((t) => t.name));
11479
+ for (const [tagName, tagDef] of globalTags) if (!projectTagNames.has(tagName)) project.config.tags.push(tagDef);
11480
+ }
11481
+ }
11482
+
11282
11483
  const types = {
11283
11484
  'application/andrew-inset': ['ez'],
11284
11485
  'application/appinstaller': ['appinstaller'],
@@ -11918,6 +12119,9 @@ class VitestWatcher {
11918
12119
  this._onRerun.push(cb);
11919
12120
  return this;
11920
12121
  }
12122
+ close() {
12123
+ this.vitest.vite.watcher.close();
12124
+ }
11921
12125
  unregisterWatcher = noop;
11922
12126
  registerWatcher() {
11923
12127
  const watcher = this.vitest.vite.watcher;
@@ -12017,7 +12221,9 @@ class VitestWatcher {
12017
12221
  }
12018
12222
  if (this.handleSetupFile(filepath)) return true;
12019
12223
  const projects = this.vitest.projects.filter((project) => {
12020
- return (project.browser?.vite.moduleGraph || project.vite.moduleGraph).getModulesByFile(filepath)?.size;
12224
+ return project._getViteEnvironments().some(({ moduleGraph }) => {
12225
+ return moduleGraph.getModulesByFile(filepath)?.size;
12226
+ });
12021
12227
  });
12022
12228
  if (!projects.length) {
12023
12229
  // if there are no modules it's possible that server was restarted
@@ -12030,8 +12236,8 @@ class VitestWatcher {
12030
12236
  }
12031
12237
  const files = [];
12032
12238
  for (const project of projects) {
12033
- const mods = project.browser?.vite.moduleGraph.getModulesByFile(filepath) || project.vite.moduleGraph.getModulesByFile(filepath);
12034
- if (!mods || !mods.size) continue;
12239
+ const environmentMods = project._getViteEnvironments().map(({ moduleGraph }) => moduleGraph.getModulesByFile(filepath));
12240
+ if (!environmentMods.length) continue;
12035
12241
  this.invalidates.add(filepath);
12036
12242
  // one of test files that we already run, or one of test files that we can run
12037
12243
  if (this.vitest.state.filesMap.has(filepath) || project._isCachedTestFile(filepath)) {
@@ -12040,7 +12246,7 @@ class VitestWatcher {
12040
12246
  continue;
12041
12247
  }
12042
12248
  let rerun = false;
12043
- for (const mod of mods) mod.importers.forEach((i) => {
12249
+ for (const mods of environmentMods) for (const mod of mods || []) mod.importers.forEach((i) => {
12044
12250
  if (!i.file) return;
12045
12251
  if (this.handleFileChanged(i.file)) rerun = true;
12046
12252
  });
@@ -12102,6 +12308,7 @@ class Vitest {
12102
12308
  /** @internal */ reporters = [];
12103
12309
  /** @internal */ runner;
12104
12310
  /** @internal */ _testRun = void 0;
12311
+ /** @internal */ _config;
12105
12312
  /** @internal */ _resolver;
12106
12313
  /** @internal */ _fetcher;
12107
12314
  /** @internal */ _fsCache;
@@ -12111,7 +12318,6 @@ class Vitest {
12111
12318
  restartsCount = 0;
12112
12319
  specifications;
12113
12320
  pool;
12114
- _config;
12115
12321
  _vite;
12116
12322
  _state;
12117
12323
  _cache;
@@ -12199,7 +12405,7 @@ class Vitest {
12199
12405
  this._fsCache = new FileSystemModuleCache(this);
12200
12406
  this._fetcher = createFetchModuleFunction(this._resolver, this._config, this._fsCache, this._traces, this._tmpDir);
12201
12407
  const environment = server.environments.__vitest__;
12202
- this.runner = new ServerModuleRunner(environment, this._fetcher, resolved);
12408
+ this.runner = resolved.experimental.viteModuleRunner === false ? new NativeModuleRunner(resolved.root) : new ServerModuleRunner(environment, this._fetcher, resolved);
12203
12409
  if (this.config.watch) {
12204
12410
  // hijack server restart
12205
12411
  const serverRestart = server.restart;
@@ -12248,6 +12454,9 @@ class Vitest {
12248
12454
  }
12249
12455
  if (!this.coreWorkspaceProject) this.coreWorkspaceProject = TestProject._createBasicProject(this);
12250
12456
  if (this.config.testNamePattern) this.configOverride.testNamePattern = this.config.testNamePattern;
12457
+ // populate will merge all configs into every project,
12458
+ // we don't want that when just listing tags
12459
+ if (!this.config.listTags) populateProjectsTags(this.coreWorkspaceProject, this.projects);
12251
12460
  this.reporters = resolved.mode === "benchmark" ? await createBenchmarkReporters(toArray(resolved.benchmark?.reporters), this.runner) : await createReporters(resolved.reporters, this);
12252
12461
  await this._fsCache.ensureCacheIntegrity();
12253
12462
  await Promise.all([...this._onSetServer.map((fn) => fn()), this._traces.waitInit()]);
@@ -12257,6 +12466,24 @@ class Vitest {
12257
12466
  if (this.configOverride.coverage?.enabled === false) return null;
12258
12467
  return this._coverageProvider;
12259
12468
  }
12469
+ async listTags() {
12470
+ const listTags = this.config.listTags;
12471
+ if (typeof listTags === "boolean") this.logger.printTags();
12472
+ else if (listTags === "json") if (![this.getRootProject(), ...this.projects].some((p) => p.config.tags && p.config.tags.length > 0)) {
12473
+ process.exitCode = 1;
12474
+ this.logger.printNoTestTagsFound();
12475
+ } else {
12476
+ const manifest = {
12477
+ tags: this.config.tags,
12478
+ projects: this.projects.filter((p) => p !== this.coreWorkspaceProject).map((p) => ({
12479
+ name: p.name,
12480
+ tags: p.config.tags
12481
+ }))
12482
+ };
12483
+ this.logger.log(JSON.stringify(manifest, null, 2));
12484
+ }
12485
+ else throw new Error(`Unknown value for "test.listTags": ${listTags}`);
12486
+ }
12260
12487
  async enableCoverage() {
12261
12488
  this.configOverride.coverage = {};
12262
12489
  this.configOverride.coverage.enabled = true;
@@ -12476,7 +12703,7 @@ class Vitest {
12476
12703
  }
12477
12704
  this.filenamePattern = filters && filters?.length > 0 ? filters : void 0;
12478
12705
  startSpan.setAttribute("vitest.start.filters", this.filenamePattern || []);
12479
- const files = await this._traces.$("vitest.config.resolve_include_glob", async () => {
12706
+ const specifications = await this._traces.$("vitest.config.resolve_include_glob", async () => {
12480
12707
  const specifications = await this.specifications.getRelevantTestSpecifications(filters);
12481
12708
  startSpan.setAttribute("vitest.start.specifications", specifications.map((s) => {
12482
12709
  const relativeModuleId = relative(s.project.config.root, s.moduleId);
@@ -12486,7 +12713,7 @@ class Vitest {
12486
12713
  return specifications;
12487
12714
  });
12488
12715
  // if run with --changed, don't exit if no tests are found
12489
- if (!files.length) {
12716
+ if (!specifications.length) {
12490
12717
  await this._traces.$("vitest.test_run", async () => {
12491
12718
  await this._testRun.start([]);
12492
12719
  const coverage = await this.coverageProvider?.generateCoverage?.({ allTestsRun: true });
@@ -12500,10 +12727,10 @@ class Vitest {
12500
12727
  testModules: [],
12501
12728
  unhandledErrors: []
12502
12729
  };
12503
- if (files.length) {
12730
+ if (specifications.length) {
12504
12731
  // populate once, update cache on watch
12505
- await this.cache.stats.populateStats(this.config.root, files);
12506
- testModules = await this.runFiles(files, true);
12732
+ await this.cache.stats.populateStats(this.config.root, specifications);
12733
+ testModules = await this.runFiles(specifications, true);
12507
12734
  }
12508
12735
  if (this.config.watch) await this.report("onWatcherStart");
12509
12736
  return testModules;
@@ -12562,6 +12789,19 @@ class Vitest {
12562
12789
  return this.runFiles(specifications, allTestsRun);
12563
12790
  }
12564
12791
  /**
12792
+ * Runs tests for the given file paths. This does not trigger `onWatcher*` events.
12793
+ * @param filepaths A list of file paths to run tests for.
12794
+ * @param allTestsRun Indicates whether all tests were run. This only matters for coverage.
12795
+ */
12796
+ async runTestFiles(filepaths, allTestsRun = false) {
12797
+ const specifications = await this.specifications.getRelevantTestSpecifications(filepaths);
12798
+ if (!specifications.length) return {
12799
+ testModules: [],
12800
+ unhandledErrors: []
12801
+ };
12802
+ return this.runFiles(specifications, allTestsRun);
12803
+ }
12804
+ /**
12565
12805
  * Rerun files and trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` events.
12566
12806
  * @param specifications A list of specifications to run.
12567
12807
  * @param allTestsRun Indicates whether all tests were run. This only matters for coverage.
@@ -12746,8 +12986,12 @@ class Vitest {
12746
12986
  async rerunTask(id) {
12747
12987
  const task = this.state.idMap.get(id);
12748
12988
  if (!task) throw new Error(`Task ${id} was not found`);
12749
- const taskNamePattern = task.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12750
- await this.changeNamePattern(taskNamePattern, [task.file.filepath], "tasks" in task ? "rerun suite" : "rerun test");
12989
+ const reportedTask = this.state.getReportedEntityById(id);
12990
+ if (!reportedTask) throw new Error(`Test specification for task ${id} was not found`);
12991
+ const specifications = [reportedTask.toTestSpecification()];
12992
+ await Promise.all([this.report("onWatcherRerun", [task.file.filepath], "tasks" in task ? "rerun suite" : "rerun test"), ...this._onUserTestsRerun.map((fn) => fn(specifications))]);
12993
+ await this.runFiles(specifications, false);
12994
+ await this.report("onWatcherStart", ["module" in reportedTask ? reportedTask.module.task : reportedTask.task]);
12751
12995
  }
12752
12996
  /** @internal */
12753
12997
  async changeProjectName(pattern) {
@@ -13055,7 +13299,8 @@ async function VitestPlugin(options = {}, vitest = new Vitest("test", deepClone(
13055
13299
  // store defines for globalThis to make them
13056
13300
  // reassignable when running in worker in src/runtime/setup.ts
13057
13301
  const originalDefine = { ...viteConfig.define };
13058
- options.defines = deleteDefineConfig(viteConfig);
13302
+ const defines = deleteDefineConfig(viteConfig);
13303
+ options.defines = defines;
13059
13304
  options.viteDefine = originalDefine;
13060
13305
  let open = false;
13061
13306
  if (testConfig.ui && testConfig.open) open = testConfig.uiBase ?? "/__vitest__/";
@@ -13510,7 +13755,8 @@ async function startVitest(mode, cliFilters = [], options = {}, viteOverrides, v
13510
13755
  else ctx.start(cliFilters);
13511
13756
  });
13512
13757
  try {
13513
- if (ctx.config.clearCache) await ctx.experimental_clearCache();
13758
+ if (ctx.config.listTags) await ctx.listTags();
13759
+ else if (ctx.config.clearCache) await ctx.experimental_clearCache();
13514
13760
  else if (ctx.config.mergeReports) await ctx.mergeReports();
13515
13761
  else if (ctx.config.standalone) await ctx.init();
13516
13762
  else await ctx.start(cliFilters);