vitest 4.0.10 → 4.0.12

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 (51) hide show
  1. package/dist/browser.d.ts +3 -3
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{base.BFVArrYW.js → base.CDEiaaLz.js} +46 -39
  4. package/dist/chunks/{browser.d.DnU_kh8a.d.ts → browser.d.Bq3zc1l_.d.ts} +1 -1
  5. package/dist/chunks/{cac.D9CYcNPM.js → cac.D9QaLeSz.js} +21 -7
  6. package/dist/chunks/{cli-api.RnIE1JbW.js → cli-api.BJh-POxZ.js} +914 -297
  7. package/dist/chunks/{coverage.BUlIqJrL.js → coverage.CtyeYmKM.js} +7 -1
  8. package/dist/chunks/{creator.DU9qFjsW.js → creator.DAmOKTvJ.js} +3 -3
  9. package/dist/chunks/{global.d.BQDgW9Pr.d.ts → global.d.Dheepru6.d.ts} +1 -1
  10. package/dist/chunks/{globals.NLOzC_A5.js → globals.C0izxiX3.js} +1 -1
  11. package/dist/chunks/{index.B8lJfb0J.js → index.CMvpbrsJ.js} +1 -1
  12. package/dist/chunks/{index.Dua7TZg_.js → index.CQwQ_SLL.js} +16 -4
  13. package/dist/chunks/{index.DZ-mI_Nm.js → index.DBx1AtPJ.js} +2 -1
  14. package/dist/chunks/{index.BYek7GgP.js → index.DWDW6mLz.js} +10 -1
  15. package/dist/chunks/{init-forks.BZSlxfwV.js → init-forks.FphdQhPI.js} +5 -5
  16. package/dist/chunks/{init-threads.CwE2n-Bv.js → init-threads.BfqfWDNi.js} +3 -3
  17. package/dist/chunks/{init.Cz2kTB9a.js → init.D-GGeAxo.js} +59 -21
  18. package/dist/chunks/{moduleRunner.d.BxT-OjLR.d.ts → moduleRunner.d.RBEiFdiW.d.ts} +5 -1
  19. package/dist/chunks/plugin.d.DGpEw-QV.d.ts +38 -0
  20. package/dist/chunks/{reporters.d.keG-yFSu.d.ts → reporters.d.C2PtoEFY.d.ts} +107 -47
  21. package/dist/chunks/{setup-common.BOzbXE3x.js → setup-common.DGHc_BUK.js} +1 -1
  22. package/dist/chunks/{startModuleRunner.DLjmA_wU.js → startModuleRunner.BEYtrq5Y.js} +97 -49
  23. package/dist/chunks/{test.BPErLMrw.js → test.DqQZzsWf.js} +14 -4
  24. package/dist/chunks/traces.U4xDYhzZ.js +172 -0
  25. package/dist/chunks/{config.d.BTfZNUu9.d.ts → traces.d.Brik_NWu.d.ts} +21 -1
  26. package/dist/chunks/{vm.wSHjz-et.js → vm.tWlKAMXr.js} +12 -8
  27. package/dist/chunks/{worker.d.ZGohxCEd.d.ts → worker.d.Dxl5oW0C.d.ts} +4 -4
  28. package/dist/cli.js +2 -2
  29. package/dist/config.d.ts +7 -7
  30. package/dist/coverage.d.ts +5 -5
  31. package/dist/coverage.js +2 -2
  32. package/dist/index.d.ts +11 -10
  33. package/dist/index.js +2 -2
  34. package/dist/module-evaluator.d.ts +3 -3
  35. package/dist/module-evaluator.js +14 -1
  36. package/dist/module-runner.js +2 -1
  37. package/dist/node.d.ts +10 -10
  38. package/dist/node.js +8 -7
  39. package/dist/reporters.d.ts +5 -5
  40. package/dist/reporters.js +2 -2
  41. package/dist/runners.d.ts +4 -1
  42. package/dist/runners.js +1 -1
  43. package/dist/worker.d.ts +7 -7
  44. package/dist/worker.js +8 -7
  45. package/dist/workers/forks.js +9 -8
  46. package/dist/workers/runVmTests.js +22 -20
  47. package/dist/workers/threads.js +9 -8
  48. package/dist/workers/vmForks.js +5 -4
  49. package/dist/workers/vmThreads.js +5 -4
  50. package/package.json +17 -12
  51. package/dist/chunks/plugin.d.C6KrdvNG.d.ts +0 -9
@@ -1,21 +1,21 @@
1
- import fs, { promises as promises$1, existsSync, mkdirSync, readFileSync, statSync, readdirSync, writeFileSync } from 'node:fs';
1
+ import fs, { promises, existsSync, mkdirSync, readFileSync, statSync, readdirSync, writeFileSync } from 'node:fs';
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, isExternalUrl, unwrapId, nanoid, withTrailingSlash, cleanUrl, wrapId, toArray, deepMerge, deepClone, isPrimitive, notNullish } from '@vitest/utils/helpers';
5
+ import { noop, createDefer, slash, isExternalUrl, unwrapId, withTrailingSlash, cleanUrl, wrapId, 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.BUlIqJrL.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.CtyeYmKM.js';
8
8
  import * as vite from 'vite';
9
- import { parseAst, fetchModule, version, searchForWorkspaceRoot, mergeConfig, createServer } from 'vite';
9
+ import { parseAst, searchForWorkspaceRoot, fetchModule, version, mergeConfig, createServer } from 'vite';
10
10
  import { A as API_PATH, c as configFiles, d as defaultBrowserPort, a as defaultPort } from './constants.D_Q9UYh-.js';
11
11
  import * as nodeos from 'node:os';
12
12
  import nodeos__default, { tmpdir } from 'node:os';
13
13
  import { generateHash as generateHash$1, 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.D9CYcNPM.js';
15
+ import { v as version$1 } from './cac.D9QaLeSz.js';
16
16
  import { performance as performance$1 } from 'node:perf_hooks';
17
17
  import { c as createBirpc } from './index.0kCJoeWi.js';
18
- import { p as parse, d as stringify, e as TraceMap, o as originalPositionFor, h as ancestor, i as printError, f as formatProjectName, w as withLabel, j as errorBanner, k as divider, l as Typechecker, m as generateCodeFrame, n as createDefinesScript, R as ReportersMap, B as BlobReporter, r as readBlobs, q as convertTasksToEvents, H as HangingProcessReporter } from './index.BYek7GgP.js';
18
+ import { p as parse, d as stringify, e as TraceMap, o as originalPositionFor, h as ancestor, i as printError, f as formatProjectName, w as withLabel, j as errorBanner, k as divider, l as Typechecker, m as generateCodeFrame, n as createDefinesScript, R as ReportersMap, B as BlobReporter, r as readBlobs, q as convertTasksToEvents, H as HangingProcessReporter } from './index.DWDW6mLz.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,12 +29,13 @@ 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 { T as Traces } from './traces.U4xDYhzZ.js';
32
33
  import createDebug from 'debug';
33
- import { readFile, writeFile, rename, stat, unlink, rm, mkdir, copyFile } from 'node:fs/promises';
34
+ import { rm, readFile, writeFile, rename, stat, unlink, mkdir, copyFile } from 'node:fs/promises';
35
+ import c from 'tinyrainbow';
34
36
  import { VitestModuleEvaluator } from '#module-evaluator';
35
37
  import { ModuleRunner } from 'vite/module-runner';
36
38
  import { Console } from 'node:console';
37
- import c from 'tinyrainbow';
38
39
  import { highlight } from '@vitest/utils/highlight';
39
40
  import { createRequire, isBuiltin, builtinModules } from 'node:module';
40
41
  import url, { fileURLToPath, pathToFileURL } from 'node:url';
@@ -51,7 +52,7 @@ import { c as configDefaults } from './defaults.BOqNVLsY.js';
51
52
  import { KNOWN_ASSET_RE } from '@vitest/utils/constants';
52
53
  import { findNearestPackageData } from '@vitest/utils/resolver';
53
54
  import * as esModuleLexer from 'es-module-lexer';
54
- import { a as BenchmarkReportsMap } from './index.B8lJfb0J.js';
55
+ import { a as BenchmarkReportsMap } from './index.CMvpbrsJ.js';
55
56
  import assert$1 from 'node:assert';
56
57
  import { serializeValue } from '@vitest/utils/serialize';
57
58
  import { parseErrorStacktrace } from '@vitest/utils/source-map';
@@ -5124,11 +5125,11 @@ function setup(ctx, _server) {
5124
5125
  },
5125
5126
  async readTestFile(id) {
5126
5127
  if (!ctx.state.filesMap.has(id) || !existsSync(id)) return null;
5127
- return promises$1.readFile(id, "utf-8");
5128
+ return promises.readFile(id, "utf-8");
5128
5129
  },
5129
5130
  async saveTestFile(id, content) {
5130
5131
  if (!ctx.state.filesMap.has(id) || !existsSync(id)) throw new Error(`Test file "${id}" was not registered, so it cannot be updated using the API.`);
5131
- return promises$1.writeFile(id, content, "utf-8");
5132
+ return promises.writeFile(id, content, "utf-8");
5132
5133
  },
5133
5134
  async rerun(files, resetTestNamePattern) {
5134
5135
  await ctx.rerunFiles(files, void 0, true, resetTestNamePattern);
@@ -5150,7 +5151,7 @@ function setup(ctx, _server) {
5150
5151
  const result = browser ? await project.browser.vite.transformRequest(id) : await project.vite.transformRequest(id);
5151
5152
  if (result) {
5152
5153
  try {
5153
- result.source = result.source || await promises$1.readFile(id, "utf-8");
5154
+ result.source = result.source || await promises.readFile(id, "utf-8");
5154
5155
  } catch {}
5155
5156
  return result;
5156
5157
  }
@@ -5225,6 +5226,12 @@ class WebSocketReporter {
5225
5226
  client.onTestAnnotate?.(testCase.id, annotation)?.catch?.(noop);
5226
5227
  });
5227
5228
  }
5229
+ async onTestCaseArtifactRecord(testCase, artifact) {
5230
+ if (this.clients.size === 0) return;
5231
+ this.clients.forEach((client) => {
5232
+ client.onTestArtifactRecord?.(testCase.id, artifact)?.catch?.(noop);
5233
+ });
5234
+ }
5228
5235
  async onTaskUpdate(packs, events) {
5229
5236
  if (this.clients.size === 0) return;
5230
5237
  this.clients.forEach((client) => {
@@ -5500,7 +5507,8 @@ function createFileTask(testFilepath, code, requestMap, options) {
5500
5507
  dynamic: definition.dynamic,
5501
5508
  meta: {},
5502
5509
  timeout: 0,
5503
- annotations: []
5510
+ annotations: [],
5511
+ artifacts: []
5504
5512
  };
5505
5513
  definition.task = task;
5506
5514
  latestSuite.tasks.push(task);
@@ -5645,8 +5653,9 @@ class ResultsCache {
5645
5653
  cachePath = null;
5646
5654
  version;
5647
5655
  root = "/";
5648
- constructor(version) {
5649
- this.version = version;
5656
+ constructor(logger) {
5657
+ this.logger = logger;
5658
+ this.version = Vitest.version;
5650
5659
  }
5651
5660
  getCachePath() {
5652
5661
  return this.cachePath;
@@ -5658,6 +5667,15 @@ class ResultsCache {
5658
5667
  getResults(key) {
5659
5668
  return this.cache.get(key);
5660
5669
  }
5670
+ async clearCache() {
5671
+ if (this.cachePath && existsSync(this.cachePath)) {
5672
+ await rm(this.cachePath, {
5673
+ force: true,
5674
+ recursive: true
5675
+ });
5676
+ this.logger.log("[cache] cleared results cache at", this.cachePath);
5677
+ }
5678
+ }
5661
5679
  async readFromCache() {
5662
5680
  if (!this.cachePath) return;
5663
5681
  if (!fs.existsSync(this.cachePath)) return;
@@ -5710,8 +5728,8 @@ class ResultsCache {
5710
5728
  class VitestCache {
5711
5729
  results;
5712
5730
  stats = new FilesStatsCache();
5713
- constructor(version) {
5714
- this.results = new ResultsCache(version);
5731
+ constructor(logger) {
5732
+ this.results = new ResultsCache(logger);
5715
5733
  }
5716
5734
  getFileTestResults(key) {
5717
5735
  return this.results.getResults(key);
@@ -5724,93 +5742,526 @@ class VitestCache {
5724
5742
  }
5725
5743
  }
5726
5744
 
5727
- const created = /* @__PURE__ */ new Set();
5728
- const promises = /* @__PURE__ */ new Map();
5729
- function createFetchModuleFunction(resolver, tmpDir = join(tmpdir(), nanoid()), dump) {
5730
- return async (url, importer, environment, cacheFs, options) => {
5731
- // We are copy pasting Vite's externalization logic from `fetchModule` because
5732
- // we instead rely on our own `shouldExternalize` method because Vite
5733
- // doesn't support `resolve.external` in non SSR environments (jsdom/happy-dom)
5734
- if (url.startsWith("data:")) return {
5735
- externalize: url,
5736
- type: "builtin"
5737
- };
5738
- if (url === "/@vite/client" || url === "@vite/client")
5739
- // this will be stubbed
5745
+ const debugFs = createDebugger("vitest:cache:fs");
5746
+ const debugMemory = createDebugger("vitest:cache:memory");
5747
+ const cacheComment = "\n//# vitestCache=";
5748
+ const cacheCommentLength = 17;
5749
+ const METADATA_FILE = "_metadata.json";
5750
+ /**
5751
+ * @experimental
5752
+ */
5753
+ class FileSystemModuleCache {
5754
+ /**
5755
+ * Even though it's possible to override the folder of project's caches
5756
+ * We still keep a single metadata file for all projects because
5757
+ * - they can reference files between each other
5758
+ * - lockfile changes are reflected for the whole workspace, not just for a single project
5759
+ */
5760
+ rootCache;
5761
+ metadataFilePath;
5762
+ version = "1.0.0-beta.1";
5763
+ fsCacheRoots = /* @__PURE__ */ new WeakMap();
5764
+ fsEnvironmentHashMap = /* @__PURE__ */ new WeakMap();
5765
+ fsCacheKeyGenerators = /* @__PURE__ */ new Set();
5766
+ // this exists only to avoid the perf. cost of reading a file and generating a hash again
5767
+ // surprisingly, on some machines this has negligible effect
5768
+ fsCacheKeys = /* @__PURE__ */ new WeakMap();
5769
+ constructor(vitest) {
5770
+ this.vitest = vitest;
5771
+ const workspaceRoot = searchForWorkspaceRoot(vitest.vite.config.root);
5772
+ this.rootCache = vitest.config.experimental.fsModuleCachePath || join(workspaceRoot, "node_modules", ".experimental-vitest-cache");
5773
+ this.metadataFilePath = join(this.rootCache, METADATA_FILE);
5774
+ }
5775
+ defineCacheKeyGenerator(callback) {
5776
+ this.fsCacheKeyGenerators.add(callback);
5777
+ }
5778
+ async clearCache(log = true) {
5779
+ const fsCachePaths = this.vitest.projects.map((r) => {
5780
+ return r.config.experimental.fsModuleCachePath || this.rootCache;
5781
+ });
5782
+ const uniquePaths = Array.from(new Set(fsCachePaths));
5783
+ await Promise.all(uniquePaths.map((directory) => rm(directory, {
5784
+ force: true,
5785
+ recursive: true
5786
+ })));
5787
+ if (log) this.vitest.logger.log(`[cache] cleared fs module cache at ${uniquePaths.join(", ")}`);
5788
+ }
5789
+ async getCachedModule(cachedFilePath) {
5790
+ if (!existsSync(cachedFilePath)) {
5791
+ debugFs?.(`${c.red("[empty]")} ${cachedFilePath} doesn't exist, transforming by vite instead`);
5792
+ return;
5793
+ }
5794
+ const code = await readFile(cachedFilePath, "utf-8");
5795
+ const matchIndex = code.lastIndexOf(cacheComment);
5796
+ if (matchIndex === -1) {
5797
+ debugFs?.(`${c.red("[empty]")} ${cachedFilePath} exists, but doesn't have a ${cacheComment} comment, transforming by vite instead`);
5798
+ return;
5799
+ }
5800
+ const meta = this.fromBase64(code.slice(matchIndex + cacheCommentLength));
5801
+ if (meta.externalize) {
5802
+ debugFs?.(`${c.green("[read]")} ${meta.externalize} is externalized inside ${cachedFilePath}`);
5803
+ return {
5804
+ externalize: meta.externalize,
5805
+ type: meta.type
5806
+ };
5807
+ }
5808
+ debugFs?.(`${c.green("[read]")} ${meta.id} is cached in ${cachedFilePath}`);
5740
5809
  return {
5741
- externalize: "/@vite/client",
5742
- type: "module"
5810
+ id: meta.id,
5811
+ url: meta.url,
5812
+ file: meta.file,
5813
+ code,
5814
+ importers: meta.importers,
5815
+ mappings: meta.mappings
5743
5816
  };
5817
+ }
5818
+ async saveCachedModule(cachedFilePath, fetchResult, importers = [], mappings = false) {
5819
+ if ("externalize" in fetchResult) {
5820
+ debugFs?.(`${c.yellow("[write]")} ${fetchResult.externalize} is externalized inside ${cachedFilePath}`);
5821
+ await atomicWriteFile(cachedFilePath, `${cacheComment}${this.toBase64(fetchResult)}`);
5822
+ } else if ("code" in fetchResult) {
5823
+ const result = {
5824
+ file: fetchResult.file,
5825
+ id: fetchResult.id,
5826
+ url: fetchResult.url,
5827
+ importers,
5828
+ mappings
5829
+ };
5830
+ debugFs?.(`${c.yellow("[write]")} ${fetchResult.id} is cached in ${cachedFilePath}`);
5831
+ await atomicWriteFile(cachedFilePath, `${fetchResult.code}${cacheComment}${this.toBase64(result)}`);
5832
+ }
5833
+ }
5834
+ toBase64(obj) {
5835
+ const json = stringify(obj);
5836
+ return Buffer.from(json).toString("base64");
5837
+ }
5838
+ fromBase64(obj) {
5839
+ return parse(Buffer.from(obj, "base64").toString("utf-8"));
5840
+ }
5841
+ invalidateCachePath(environment, id) {
5842
+ debugFs?.(`cache for ${id} in ${environment.name} environment is invalidated`);
5843
+ this.fsCacheKeys.get(environment)?.delete(id);
5844
+ }
5845
+ invalidateAllCachePaths(environment) {
5846
+ debugFs?.(`the ${environment.name} environment cache is invalidated`);
5847
+ this.fsCacheKeys.get(environment)?.clear();
5848
+ }
5849
+ getMemoryCachePath(environment, id) {
5850
+ const result = this.fsCacheKeys.get(environment)?.get(id);
5851
+ if (result != null) debugMemory?.(`${c.green("[read]")} ${id} was cached in ${result}`);
5852
+ else if (result === null) debugMemory?.(`${c.green("[read]")} ${id} was bailed out`);
5853
+ return result;
5854
+ }
5855
+ generateCachePath(vitestConfig, environment, resolver, id, fileContent) {
5856
+ let hashString = "";
5857
+ // bail out if file has import.meta.glob because it depends on other files
5858
+ // TODO: figure out a way to still support it
5859
+ if (fileContent.includes("import.meta.glob(")) {
5860
+ this.saveMemoryCache(environment, id, null);
5861
+ debugMemory?.(`${c.yellow("[write]")} ${id} was bailed out`);
5862
+ return null;
5863
+ }
5864
+ for (const generator of this.fsCacheKeyGenerators) {
5865
+ const result = generator({
5866
+ environment,
5867
+ id,
5868
+ sourceCode: fileContent
5869
+ });
5870
+ if (typeof result === "string") hashString += result;
5871
+ if (result === false) {
5872
+ this.saveMemoryCache(environment, id, null);
5873
+ debugMemory?.(`${c.yellow("[write]")} ${id} was bailed out by a custom generator`);
5874
+ return null;
5875
+ }
5876
+ }
5877
+ const config = environment.config;
5878
+ // coverage provider is dynamic, so we also clear the whole cache if
5879
+ // vitest.enableCoverage/vitest.disableCoverage is called
5880
+ const coverageAffectsCache = String(this.vitest.config.coverage.enabled && this.vitest.coverageProvider?.requiresTransform?.(id));
5881
+ let cacheConfig = this.fsEnvironmentHashMap.get(environment);
5882
+ if (!cacheConfig) {
5883
+ cacheConfig = JSON.stringify({
5884
+ root: config.root,
5885
+ base: config.base,
5886
+ mode: config.mode,
5887
+ consumer: config.consumer,
5888
+ resolve: config.resolve,
5889
+ plugins: config.plugins.map((p) => p.name),
5890
+ configFileDependencies: config.configFileDependencies.map((file) => tryReadFileSync(file)),
5891
+ environment: environment.name,
5892
+ css: vitestConfig.css,
5893
+ resolver: {
5894
+ inline: resolver.options.inline,
5895
+ external: resolver.options.external,
5896
+ inlineFiles: resolver.options.inlineFiles,
5897
+ moduleDirectories: resolver.options.moduleDirectories
5898
+ }
5899
+ }, (_, value) => {
5900
+ if (typeof value === "function" || value instanceof RegExp) return value.toString();
5901
+ return value;
5902
+ });
5903
+ this.fsEnvironmentHashMap.set(environment, cacheConfig);
5904
+ }
5905
+ hashString += id + fileContent + (process.env.NODE_ENV ?? "") + this.version + cacheConfig + coverageAffectsCache;
5906
+ const cacheKey = hash("sha1", hashString, "hex");
5907
+ let cacheRoot = this.fsCacheRoots.get(vitestConfig);
5908
+ if (cacheRoot == null) {
5909
+ cacheRoot = vitestConfig.experimental.fsModuleCachePath || this.rootCache;
5910
+ if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { recursive: true });
5911
+ }
5912
+ const fsResultPath = join(cacheRoot, cacheKey);
5913
+ debugMemory?.(`${c.yellow("[write]")} ${id} generated a cache in ${fsResultPath}`);
5914
+ this.saveMemoryCache(environment, id, fsResultPath);
5915
+ return fsResultPath;
5916
+ }
5917
+ saveMemoryCache(environment, id, cache) {
5918
+ let environmentKeys = this.fsCacheKeys.get(environment);
5919
+ if (!environmentKeys) {
5920
+ environmentKeys = /* @__PURE__ */ new Map();
5921
+ this.fsCacheKeys.set(environment, environmentKeys);
5922
+ }
5923
+ environmentKeys.set(id, cache);
5924
+ }
5925
+ async readMetadata() {
5926
+ // metadata is shared between every projects in the workspace, so we ignore project's fsModuleCachePath
5927
+ if (!existsSync(this.metadataFilePath)) return;
5928
+ try {
5929
+ const content = await readFile(this.metadataFilePath, "utf-8");
5930
+ return JSON.parse(content);
5931
+ } catch {}
5932
+ }
5933
+ // before vitest starts running tests, we check that the lockfile wasn't updated
5934
+ // if it was, we nuke the previous cache in case a custom plugin was updated
5935
+ // or a new version of vite/vitest is installed
5936
+ // for the same reason we also cache config file content, but that won't catch changes made in external plugins
5937
+ async ensureCacheIntegrity() {
5938
+ if (![this.vitest.getRootProject(), ...this.vitest.projects].some((p) => p.config.experimental.fsModuleCache)) return;
5939
+ const metadata = await this.readMetadata();
5940
+ const currentLockfileHash = getLockfileHash(this.vitest.vite.config.root);
5941
+ // no metadata found, just store a new one, don't reset the cache
5942
+ if (!metadata) {
5943
+ if (!existsSync(this.rootCache)) mkdirSync(this.rootCache, { recursive: true });
5944
+ debugFs?.(`fs metadata file was created with hash ${currentLockfileHash}`);
5945
+ await writeFile(this.metadataFilePath, JSON.stringify({ lockfileHash: currentLockfileHash }, null, 2), "utf-8");
5946
+ return;
5947
+ }
5948
+ // if lockfile didn't change, don't do anything
5949
+ if (metadata.lockfileHash === currentLockfileHash) return;
5950
+ // lockfile changed, let's clear all caches
5951
+ await this.clearCache(false);
5952
+ this.vitest.vite.config.logger.info(`fs cache was cleared because lockfile has changed`, {
5953
+ timestamp: true,
5954
+ environment: c.yellow("[vitest]")
5955
+ });
5956
+ debugFs?.(`fs cache was cleared because lockfile has changed`);
5957
+ }
5958
+ }
5959
+ /**
5960
+ * Performs an atomic write operation using the write-then-rename pattern.
5961
+ *
5962
+ * Why we need this:
5963
+ * - Ensures file integrity by never leaving partially written files on disk
5964
+ * - Prevents other processes from reading incomplete data during writes
5965
+ * - Particularly important for test files where incomplete writes could cause test failures
5966
+ *
5967
+ * The implementation writes to a temporary file first, then renames it to the target path.
5968
+ * This rename operation is atomic on most filesystems (including POSIX-compliant ones),
5969
+ * guaranteeing that other processes will only ever see the complete file.
5970
+ *
5971
+ * Added in https://github.com/vitest-dev/vitest/pull/7531
5972
+ */
5973
+ async function atomicWriteFile(realFilePath, data) {
5974
+ const tmpFilePath = join(dirname(realFilePath), `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`);
5975
+ try {
5976
+ await writeFile(tmpFilePath, data, "utf-8");
5977
+ await rename(tmpFilePath, realFilePath);
5978
+ } finally {
5979
+ try {
5980
+ if (await stat(tmpFilePath)) await unlink(tmpFilePath);
5981
+ } catch {}
5982
+ }
5983
+ }
5984
+ // lockfile hash resolution taken from vite
5985
+ // since this is experimental, we don't ask to expose it
5986
+ const lockfileFormats = [
5987
+ {
5988
+ path: "node_modules/.package-lock.json",
5989
+ checkPatchesDir: "patches",
5990
+ manager: "npm"
5991
+ },
5992
+ {
5993
+ path: "node_modules/.yarn-state.yml",
5994
+ checkPatchesDir: false,
5995
+ manager: "yarn"
5996
+ },
5997
+ {
5998
+ path: ".pnp.cjs",
5999
+ checkPatchesDir: ".yarn/patches",
6000
+ manager: "yarn"
6001
+ },
6002
+ {
6003
+ path: ".pnp.js",
6004
+ checkPatchesDir: ".yarn/patches",
6005
+ manager: "yarn"
6006
+ },
6007
+ {
6008
+ path: "node_modules/.yarn-integrity",
6009
+ checkPatchesDir: "patches",
6010
+ manager: "yarn"
6011
+ },
6012
+ {
6013
+ path: "node_modules/.pnpm/lock.yaml",
6014
+ checkPatchesDir: false,
6015
+ manager: "pnpm"
6016
+ },
6017
+ {
6018
+ path: ".rush/temp/shrinkwrap-deps.json",
6019
+ checkPatchesDir: false,
6020
+ manager: "pnpm"
6021
+ },
6022
+ {
6023
+ path: "bun.lock",
6024
+ checkPatchesDir: "patches",
6025
+ manager: "bun"
6026
+ },
6027
+ {
6028
+ path: "bun.lockb",
6029
+ checkPatchesDir: "patches",
6030
+ manager: "bun"
6031
+ }
6032
+ ].sort((_, { manager }) => {
6033
+ return process.env.npm_config_user_agent?.startsWith(manager) ? 1 : -1;
6034
+ });
6035
+ const lockfilePaths = lockfileFormats.map((l) => l.path);
6036
+ function getLockfileHash(root) {
6037
+ const lockfilePath = lookupFile(root, lockfilePaths);
6038
+ let content = lockfilePath ? fs.readFileSync(lockfilePath, "utf-8") : "";
6039
+ if (lockfilePath) {
6040
+ const normalizedLockfilePath = lockfilePath.replaceAll("\\", "/");
6041
+ const lockfileFormat = lockfileFormats.find((f) => normalizedLockfilePath.endsWith(f.path));
6042
+ if (lockfileFormat.checkPatchesDir) {
6043
+ const stat = tryStatSync(join(lockfilePath.slice(0, -lockfileFormat.path.length), lockfileFormat.checkPatchesDir));
6044
+ if (stat?.isDirectory()) content += stat.mtimeMs.toString();
6045
+ }
6046
+ }
6047
+ return hash("sha256", content, "hex").substring(0, 8).padEnd(8, "_");
6048
+ }
6049
+ function lookupFile(dir, fileNames) {
6050
+ while (dir) {
6051
+ for (const fileName of fileNames) {
6052
+ const fullPath = join(dir, fileName);
6053
+ if (tryStatSync(fullPath)?.isFile()) return fullPath;
6054
+ }
6055
+ const parentDir = dirname(dir);
6056
+ if (parentDir === dir) return;
6057
+ dir = parentDir;
6058
+ }
6059
+ }
6060
+ function tryReadFileSync(file) {
6061
+ try {
6062
+ return readFileSync(file, "utf-8");
6063
+ } catch {
6064
+ return "";
6065
+ }
6066
+ }
6067
+ function tryStatSync(file) {
6068
+ try {
6069
+ // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
6070
+ return fs.statSync(file, { throwIfNoEntry: false });
6071
+ } catch {}
6072
+ }
6073
+
6074
+ const saveCachePromises = /* @__PURE__ */ new Map();
6075
+ const readFilePromises = /* @__PURE__ */ new Map();
6076
+ class ModuleFetcher {
6077
+ tmpDirectories = /* @__PURE__ */ new Set();
6078
+ fsCacheEnabled;
6079
+ constructor(resolver, config, fsCache, traces, tmpProjectDir) {
6080
+ this.resolver = resolver;
6081
+ this.config = config;
6082
+ this.fsCache = fsCache;
6083
+ this.traces = traces;
6084
+ this.tmpProjectDir = tmpProjectDir;
6085
+ this.fsCacheEnabled = config.experimental?.fsModuleCache === true;
6086
+ }
6087
+ async fetch(trace, url, importer, environment, makeTmpCopies, options) {
6088
+ if (url.startsWith("data:")) {
6089
+ trace.setAttribute("vitest.module.external", url);
6090
+ return {
6091
+ externalize: url,
6092
+ type: "builtin"
6093
+ };
6094
+ }
6095
+ if (url === "/@vite/client" || url === "@vite/client") {
6096
+ trace.setAttribute("vitest.module.external", url);
6097
+ return {
6098
+ externalize: "/@vite/client",
6099
+ type: "module"
6100
+ };
6101
+ }
5744
6102
  const isFileUrl = url.startsWith("file://");
5745
- if (isExternalUrl(url) && !isFileUrl) return {
5746
- externalize: url,
5747
- type: "network"
5748
- };
5749
- // Vite does the same in `fetchModule`, but we want to externalize modules ourselves,
5750
- // so we do this first to resolve the module and check its `id`. The next call of
5751
- // `ensureEntryFromUrl` inside `fetchModule` is cached and should take no time
5752
- // This also makes it so externalized modules are inside the module graph.
6103
+ if (isExternalUrl(url) && !isFileUrl) {
6104
+ trace.setAttribute("vitest.module.external", url);
6105
+ return {
6106
+ externalize: url,
6107
+ type: "network"
6108
+ };
6109
+ }
5753
6110
  const moduleGraphModule = await environment.moduleGraph.ensureEntryFromUrl(unwrapId(url));
5754
6111
  const cached = !!moduleGraphModule.transformResult;
5755
- // if url is already cached, we can just confirm it's also cached on the server
6112
+ if (moduleGraphModule.file) trace.setAttribute("code.file.path", moduleGraphModule.file);
5756
6113
  if (options?.cached && cached) return { cache: true };
5757
- if (moduleGraphModule.id) {
5758
- const externalize = await resolver.shouldExternalize(moduleGraphModule.id);
5759
- if (externalize) return {
5760
- externalize,
5761
- type: "module"
5762
- };
6114
+ const cachePath = await this.getCachePath(environment, moduleGraphModule);
6115
+ // full fs caching is disabled, but we still want to keep tmp files if makeTmpCopies is enabled
6116
+ // this is primarily used by the forks pool to avoid using process.send(bigBuffer)
6117
+ if (cachePath == null) {
6118
+ const result = await this.fetchAndProcess(environment, url, importer, moduleGraphModule, options);
6119
+ this.recordResult(trace, result);
6120
+ if (!makeTmpCopies || !("code" in result)) return result;
6121
+ const transformResult = moduleGraphModule.transformResult;
6122
+ const tmpPath = transformResult && Reflect.get(transformResult, "_vitest_tmp");
6123
+ if (typeof tmpPath === "string") return getCachedResult(result, tmpPath);
6124
+ const tmpDir = join(this.tmpProjectDir, environment.name);
6125
+ if (!this.tmpDirectories.has(tmpDir)) {
6126
+ if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true });
6127
+ this.tmpDirectories.add(tmpDir);
6128
+ }
6129
+ const tmpFile = join(tmpDir, hash("sha1", result.id, "hex"));
6130
+ return this.cacheResult(result, tmpFile).then((result) => {
6131
+ if (transformResult) Reflect.set(transformResult, "_vitest_tmp", tmpFile);
6132
+ return result;
6133
+ });
5763
6134
  }
5764
- let moduleRunnerModule;
5765
- if (dump?.dumpFolder && dump.readFromDump) {
5766
- const path = resolve(dump?.dumpFolder, url.replace(/[^\w+]/g, "-"));
5767
- if (existsSync(path)) {
5768
- const code = await readFile(path, "utf-8");
5769
- const matchIndex = code.lastIndexOf("\n//");
5770
- if (matchIndex !== -1) {
5771
- const { id, file } = JSON.parse(code.slice(matchIndex + 4));
5772
- moduleRunnerModule = {
5773
- code,
5774
- id,
5775
- url,
5776
- file,
5777
- invalidate: false
5778
- };
5779
- }
6135
+ if (saveCachePromises.has(cachePath)) return saveCachePromises.get(cachePath).then((result) => {
6136
+ this.recordResult(trace, result);
6137
+ return result;
6138
+ });
6139
+ const cachedModule = await this.getCachedModule(cachePath, environment, moduleGraphModule);
6140
+ if (cachedModule) {
6141
+ this.recordResult(trace, cachedModule);
6142
+ return cachedModule;
6143
+ }
6144
+ const result = await this.fetchAndProcess(environment, url, importer, moduleGraphModule, options);
6145
+ const importers = this.getSerializedDependencies(moduleGraphModule);
6146
+ const map = moduleGraphModule.transformResult?.map;
6147
+ const mappings = map && !("version" in map) && map.mappings === "";
6148
+ return this.cacheResult(result, cachePath, importers, !!mappings);
6149
+ }
6150
+ getSerializedDependencies(node) {
6151
+ const dependencies = [];
6152
+ node.importers.forEach((importer) => {
6153
+ if (importer.id) dependencies.push(importer.id);
6154
+ });
6155
+ return dependencies;
6156
+ }
6157
+ recordResult(trace, result) {
6158
+ if ("externalize" in result) trace.setAttributes({
6159
+ "vitest.module.external": result.externalize,
6160
+ "vitest.fetched_module.type": result.type
6161
+ });
6162
+ if ("id" in result) {
6163
+ trace.setAttributes({
6164
+ "vitest.fetched_module.invalidate": result.invalidate,
6165
+ "vitest.fetched_module.id": result.id,
6166
+ "vitest.fetched_module.url": result.url,
6167
+ "vitest.fetched_module.cache": false
6168
+ });
6169
+ if (result.file) trace.setAttribute("code.file.path", result.file);
6170
+ }
6171
+ if ("code" in result) trace.setAttribute("vitest.fetched_module.code_length", result.code.length);
6172
+ }
6173
+ async getCachePath(environment, moduleGraphModule) {
6174
+ if (!this.fsCacheEnabled) return null;
6175
+ const moduleId = moduleGraphModule.id;
6176
+ const memoryCacheKey = this.fsCache.getMemoryCachePath(environment, moduleId);
6177
+ // undefined means there is no key in memory
6178
+ // null means the file should not be cached
6179
+ if (memoryCacheKey !== void 0) return memoryCacheKey;
6180
+ const fileContent = await this.readFileContentToCache(environment, moduleGraphModule);
6181
+ return this.fsCache.generateCachePath(this.config, environment, this.resolver, moduleGraphModule.id, fileContent);
6182
+ }
6183
+ async readFileContentToCache(environment, moduleGraphModule) {
6184
+ if (moduleGraphModule.file && !moduleGraphModule.file.startsWith("\0") && !moduleGraphModule.file.startsWith("virtual:")) {
6185
+ const result = await this.readFileConcurrently(moduleGraphModule.file);
6186
+ if (result != null) return result;
6187
+ }
6188
+ const loadResult = await environment.pluginContainer.load(moduleGraphModule.id);
6189
+ if (typeof loadResult === "string") return loadResult;
6190
+ if (loadResult != null) return loadResult.code;
6191
+ return "";
6192
+ }
6193
+ async getCachedModule(cachePath, environment, moduleGraphModule) {
6194
+ const cachedModule = await this.fsCache.getCachedModule(cachePath);
6195
+ if (cachedModule && "code" in cachedModule) {
6196
+ // keep the module graph in sync
6197
+ if (!moduleGraphModule.transformResult) {
6198
+ let map = extractSourceMap(cachedModule.code);
6199
+ if (map && cachedModule.file) map.file = cachedModule.file;
6200
+ // mappings is a special source map identifier in rollup
6201
+ if (!map && cachedModule.mappings) map = { mappings: "" };
6202
+ moduleGraphModule.transformResult = {
6203
+ code: cachedModule.code,
6204
+ map,
6205
+ ssr: true
6206
+ };
6207
+ // we populate the module graph to make the watch mode work because it relies on importers
6208
+ cachedModule.importers.forEach((importer) => {
6209
+ const environmentNode = environment.moduleGraph.getModuleById(importer);
6210
+ if (environmentNode) moduleGraphModule.importers.add(environmentNode);
6211
+ });
5780
6212
  }
6213
+ return {
6214
+ cached: true,
6215
+ file: cachedModule.file,
6216
+ id: cachedModule.id,
6217
+ tmp: cachePath,
6218
+ url: cachedModule.url,
6219
+ invalidate: false
6220
+ };
5781
6221
  }
5782
- if (!moduleRunnerModule) moduleRunnerModule = await fetchModule(environment, url, importer, {
6222
+ return cachedModule;
6223
+ }
6224
+ async fetchAndProcess(environment, url, importer, moduleGraphModule, options) {
6225
+ const externalize = await this.resolver.shouldExternalize(moduleGraphModule.id);
6226
+ if (externalize) return {
6227
+ externalize,
6228
+ type: "module"
6229
+ };
6230
+ return processResultSource(environment, await fetchModule(environment, url, importer, {
5783
6231
  ...options,
5784
6232
  inlineSourceMap: false
5785
- }).catch(handleRollupError);
5786
- const result = processResultSource(environment, moduleRunnerModule);
5787
- if (dump?.dumpFolder && "code" in result) await writeFile(resolve(dump?.dumpFolder, result.url.replace(/[^\w+]/g, "-")), `${result.code}\n// ${JSON.stringify({
5788
- id: result.id,
5789
- file: result.file
5790
- })}`, "utf-8");
5791
- if (!cacheFs || !("code" in result)) return result;
5792
- const code = result.code;
5793
- const transformResult = result.transformResult;
5794
- if (!transformResult) throw new Error(`"transformResult" in not defined. This is a bug in Vitest.`);
5795
- // to avoid serialising large chunks of code,
5796
- // we store them in a tmp file and read in the test thread
5797
- if ("_vitestTmp" in transformResult) return getCachedResult(result, Reflect.get(transformResult, "_vitestTmp"));
5798
- const dir = join(tmpDir, environment.name);
5799
- const tmp = join(dir, hash("sha1", result.id, "hex"));
5800
- if (!created.has(dir)) {
5801
- mkdirSync(dir, { recursive: true });
5802
- created.add(dir);
5803
- }
5804
- if (promises.has(tmp)) {
5805
- await promises.get(tmp);
5806
- return getCachedResult(result, tmp);
5807
- }
5808
- promises.set(tmp, atomicWriteFile(tmp, code).catch(() => writeFile(tmp, code, "utf-8")).finally(() => {
5809
- Reflect.set(transformResult, "_vitestTmp", tmp);
5810
- promises.delete(tmp);
5811
- }));
5812
- await promises.get(tmp);
5813
- return getCachedResult(result, tmp);
6233
+ }).catch(handleRollupError));
6234
+ }
6235
+ async cacheResult(result, cachePath, importers = [], mappings = false) {
6236
+ const returnResult = "code" in result ? getCachedResult(result, cachePath) : result;
6237
+ if (saveCachePromises.has(cachePath)) {
6238
+ await saveCachePromises.get(cachePath);
6239
+ return returnResult;
6240
+ }
6241
+ const savePromise = this.fsCache.saveCachedModule(cachePath, result, importers, mappings).then(() => result).finally(() => {
6242
+ saveCachePromises.delete(cachePath);
6243
+ });
6244
+ saveCachePromises.set(cachePath, savePromise);
6245
+ await savePromise;
6246
+ return returnResult;
6247
+ }
6248
+ readFileConcurrently(file) {
6249
+ if (!readFilePromises.has(file)) readFilePromises.set(
6250
+ file,
6251
+ // virtual file can have a "file" property
6252
+ readFile(file, "utf-8").catch(() => null).finally(() => {
6253
+ readFilePromises.delete(file);
6254
+ })
6255
+ );
6256
+ return readFilePromises.get(file);
6257
+ }
6258
+ }
6259
+ function createFetchModuleFunction(resolver, config, fsCache, traces, tmpProjectDir) {
6260
+ const fetcher = new ModuleFetcher(resolver, config, fsCache, traces, tmpProjectDir);
6261
+ return async (url, importer, environment, cacheFs, options, otelCarrier) => {
6262
+ await traces.waitInit();
6263
+ const context = otelCarrier ? traces.getContextFromCarrier(otelCarrier) : void 0;
6264
+ return traces.$("vitest.module.transform", context ? { context } : {}, (span) => fetcher.fetch(span, url, importer, environment, cacheFs, options));
5814
6265
  };
5815
6266
  }
5816
6267
  let SOURCEMAPPING_URL = "sourceMa";
@@ -5825,8 +6276,7 @@ function processResultSource(environment, result) {
5825
6276
  inlineSourceMap(node.transformResult);
5826
6277
  return {
5827
6278
  ...result,
5828
- code: node?.transformResult?.code || result.code,
5829
- transformResult: node?.transformResult
6279
+ code: node?.transformResult?.code || result.code
5830
6280
  };
5831
6281
  }
5832
6282
  const OTHER_SOURCE_MAP_REGEXP = new RegExp(`//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,([A-Za-z0-9+/=]+)$`, "gm");
@@ -5862,6 +6312,20 @@ function getCachedResult(result, tmp) {
5862
6312
  invalidate: result.invalidate
5863
6313
  };
5864
6314
  }
6315
+ const MODULE_RUNNER_SOURCEMAPPING_REGEXP = /* @__PURE__ */ new RegExp(`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`);
6316
+ function extractSourceMap(code) {
6317
+ const pattern = `//# ${SOURCEMAPPING_URL}=data:application/json;base64,`;
6318
+ const lastIndex = code.lastIndexOf(pattern);
6319
+ if (lastIndex === -1) return null;
6320
+ const mapString = MODULE_RUNNER_SOURCEMAPPING_REGEXP.exec(code.slice(lastIndex))?.[1];
6321
+ if (!mapString) return null;
6322
+ const sourceMap = JSON.parse(Buffer.from(mapString, "base64").toString("utf-8"));
6323
+ // remove source map mapping added by "inlineSourceMap" to keep the original behaviour of transformRequest
6324
+ if (sourceMap.mappings.startsWith("AAAA,CAAA;"))
6325
+ // 9 because we want to only remove "AAAA,CAAA", but keep ; at the start
6326
+ sourceMap.mappings = sourceMap.mappings.slice(9);
6327
+ return sourceMap;
6328
+ }
5865
6329
  // serialize rollup error on server to preserve details as a test error
5866
6330
  function handleRollupError(e) {
5867
6331
  if (e instanceof Error && ("plugin" in e || "frame" in e || "id" in e))
@@ -5880,31 +6344,6 @@ function handleRollupError(e) {
5880
6344
  };
5881
6345
  throw e;
5882
6346
  }
5883
- /**
5884
- * Performs an atomic write operation using the write-then-rename pattern.
5885
- *
5886
- * Why we need this:
5887
- * - Ensures file integrity by never leaving partially written files on disk
5888
- * - Prevents other processes from reading incomplete data during writes
5889
- * - Particularly important for test files where incomplete writes could cause test failures
5890
- *
5891
- * The implementation writes to a temporary file first, then renames it to the target path.
5892
- * This rename operation is atomic on most filesystems (including POSIX-compliant ones),
5893
- * guaranteeing that other processes will only ever see the complete file.
5894
- *
5895
- * Added in https://github.com/vitest-dev/vitest/pull/7531
5896
- */
5897
- async function atomicWriteFile(realFilePath, data) {
5898
- const tmpFilePath = join(dirname(realFilePath), `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`);
5899
- try {
5900
- await writeFile(tmpFilePath, data, "utf-8");
5901
- await rename(tmpFilePath, realFilePath);
5902
- } finally {
5903
- try {
5904
- if (await stat(tmpFilePath)) await unlink(tmpFilePath);
5905
- } catch {}
5906
- }
5907
- }
5908
6347
 
5909
6348
  // this is copy pasted from vite
5910
6349
  function normalizeResolvedIdToUrl(environment, resolvedId) {
@@ -5938,7 +6377,15 @@ class ServerModuleRunner extends ModuleRunner {
5938
6377
  if (name === "getBuiltins") return await environment.hot.handleInvoke(event);
5939
6378
  if (name !== "fetchModule") return { error: /* @__PURE__ */ new Error(`Unknown method: ${name}. Expected "fetchModule".`) };
5940
6379
  try {
5941
- return { result: await fetcher(data[0], data[1], environment, false, data[2]) };
6380
+ const result = await fetcher(data[0], data[1], environment, false, data[2]);
6381
+ if ("tmp" in result) {
6382
+ const code = await readFile(result.tmp);
6383
+ return { result: {
6384
+ ...result,
6385
+ code
6386
+ } };
6387
+ }
6388
+ return { result };
5942
6389
  } catch (error) {
5943
6390
  return { error };
5944
6391
  }
@@ -6295,7 +6742,7 @@ async function getSpecificationsEnvironments(specifications) {
6295
6742
  // reuse if projects have the same test files
6296
6743
  let code = cache.get(filepath);
6297
6744
  if (!code) {
6298
- code = await promises$1.readFile(filepath, "utf-8");
6745
+ code = await promises.readFile(filepath, "utf-8");
6299
6746
  cache.set(filepath, code);
6300
6747
  }
6301
6748
  // 1. Check for control comments in the file
@@ -6553,9 +7000,9 @@ function shouldIgnoreDebugger(provider, browser) {
6553
7000
  return browser !== "chromium";
6554
7001
  }
6555
7002
 
6556
- function createMethodsRPC(project, options = {}) {
7003
+ function createMethodsRPC(project, methodsOptions = {}) {
6557
7004
  const vitest = project.vitest;
6558
- const cacheFs = options.cacheFs ?? false;
7005
+ const cacheFs = methodsOptions.cacheFs ?? false;
6559
7006
  project.vitest.state.metadata[project.name] ??= {
6560
7007
  externalized: {},
6561
7008
  duration: {},
@@ -6564,11 +7011,11 @@ function createMethodsRPC(project, options = {}) {
6564
7011
  if (project.config.dumpDir && !existsSync(project.config.dumpDir)) mkdirSync(project.config.dumpDir, { recursive: true });
6565
7012
  project.vitest.state.metadata[project.name].dumpDir = project.config.dumpDir;
6566
7013
  return {
6567
- async fetch(url, importer, environmentName, options) {
7014
+ async fetch(url, importer, environmentName, options, otelCarrier) {
6568
7015
  const environment = project.vite.environments[environmentName];
6569
7016
  if (!environment) throw new Error(`The environment ${environmentName} was not defined in the Vite config.`);
6570
7017
  const start = performance.now();
6571
- return await project._fetcher(url, importer, environment, cacheFs, options).then((result) => {
7018
+ return await project._fetcher(url, importer, environment, cacheFs, options, otelCarrier).then((result) => {
6572
7019
  const duration = performance.now() - start;
6573
7020
  project.vitest.state.transformTime += duration;
6574
7021
  const metadata = project.vitest.state.metadata[project.name];
@@ -6609,25 +7056,25 @@ function createMethodsRPC(project, options = {}) {
6609
7056
  return { code: (await environment.transformRequest(url).catch(handleRollupError))?.code };
6610
7057
  },
6611
7058
  async onQueued(file) {
6612
- if (options.collect) vitest.state.collectFiles(project, [file]);
7059
+ if (methodsOptions.collect) vitest.state.collectFiles(project, [file]);
6613
7060
  else await vitest._testRun.enqueued(project, file);
6614
7061
  },
6615
7062
  async onCollected(files) {
6616
- if (options.collect) vitest.state.collectFiles(project, files);
7063
+ if (methodsOptions.collect) vitest.state.collectFiles(project, files);
6617
7064
  else await vitest._testRun.collected(project, files);
6618
7065
  },
6619
7066
  onAfterSuiteRun(meta) {
6620
7067
  vitest.coverageProvider?.onAfterSuiteRun(meta);
6621
7068
  },
6622
- async onTaskAnnotate(testId, annotation) {
6623
- return vitest._testRun.annotate(testId, annotation);
7069
+ async onTaskArtifactRecord(testId, artifact) {
7070
+ return vitest._testRun.recordArtifact(testId, artifact);
6624
7071
  },
6625
7072
  async onTaskUpdate(packs, events) {
6626
- if (options.collect) vitest.state.updateTasks(packs);
7073
+ if (methodsOptions.collect) vitest.state.updateTasks(packs);
6627
7074
  else await vitest._testRun.updated(packs, events);
6628
7075
  },
6629
7076
  async onUserConsoleLog(log) {
6630
- if (options.collect) vitest.state.updateUserLog(log);
7077
+ if (methodsOptions.collect) vitest.state.updateUserLog(log);
6631
7078
  else await vitest._testRun.log(log);
6632
7079
  },
6633
7080
  onUnhandledError(err, type) {
@@ -6664,6 +7111,8 @@ class PoolRunner {
6664
7111
  _eventEmitter = new EventEmitter();
6665
7112
  _offCancel;
6666
7113
  _rpc;
7114
+ _otel = null;
7115
+ _traces;
6667
7116
  get isTerminated() {
6668
7117
  return this._state === RunnerState.STOPPED;
6669
7118
  }
@@ -6677,6 +7126,21 @@ class PoolRunner {
6677
7126
  this.worker = worker;
6678
7127
  this.project = options.project;
6679
7128
  this.environment = options.environment;
7129
+ const vitest = this.project.vitest;
7130
+ this._traces = vitest._traces;
7131
+ if (this._traces.isEnabled()) {
7132
+ const { span: workerSpan, context } = this._traces.startContextSpan("vitest.worker");
7133
+ this._otel = {
7134
+ span: workerSpan,
7135
+ workerContext: context,
7136
+ files: []
7137
+ };
7138
+ this._otel.span.setAttributes({
7139
+ "vitest.worker.name": this.worker.name,
7140
+ "vitest.project": this.project.name,
7141
+ "vitest.environment": this.environment.name
7142
+ });
7143
+ }
6680
7144
  this._rpc = createBirpc(createMethodsRPC(this.project, {
6681
7145
  collect: options.method === "collect",
6682
7146
  cacheFs: worker.cacheFs
@@ -6688,13 +7152,39 @@ class PoolRunner {
6688
7152
  on: (callback) => this._eventEmitter.on("rpc", callback),
6689
7153
  timeout: -1
6690
7154
  });
6691
- this._offCancel = this.project.vitest.onCancel((reason) => this._rpc.onCancel(reason));
7155
+ this._offCancel = vitest.onCancel((reason) => this._rpc.onCancel(reason));
6692
7156
  }
6693
7157
  postMessage(message) {
6694
7158
  // Only send messages when runner is active (not fully stopped)
6695
7159
  // Allow sending during STOPPING state for the 'stop' message itself
6696
7160
  if (this._state !== RunnerState.STOPPED) return this.worker.send(message);
6697
7161
  }
7162
+ startTracesSpan(name) {
7163
+ const traces = this._traces;
7164
+ if (!this._otel) return traces.startSpan(name);
7165
+ const { span, context } = traces.startContextSpan(name, this._otel.workerContext);
7166
+ this._otel.currentContext = context;
7167
+ const end = span.end.bind(span);
7168
+ span.end = (endTime) => {
7169
+ this._otel.currentContext = void 0;
7170
+ return end(endTime);
7171
+ };
7172
+ return span;
7173
+ }
7174
+ request(method, context) {
7175
+ this._otel?.files.push(...context.files.map((f) => f.filepath));
7176
+ return this.postMessage({
7177
+ __vitest_worker_request__: true,
7178
+ type: method,
7179
+ context,
7180
+ poolId: this.poolId,
7181
+ otelCarrier: this.getOTELCarrier()
7182
+ });
7183
+ }
7184
+ getOTELCarrier() {
7185
+ const activeContext = this._otel?.currentContext || this._otel?.workerContext;
7186
+ return activeContext ? this._traces.getContextCarrier(activeContext) : void 0;
7187
+ }
6698
7188
  async start() {
6699
7189
  // Wait for any ongoing operation to complete
6700
7190
  if (this._operationLock) await this._operationLock;
@@ -6702,15 +7192,21 @@ class PoolRunner {
6702
7192
  if (this._state === RunnerState.STOPPED) throw new Error("[vitest-pool-runner]: Cannot start a stopped runner");
6703
7193
  // Create operation lock to prevent concurrent start/stop
6704
7194
  this._operationLock = createDefer();
7195
+ let startSpan;
6705
7196
  try {
6706
7197
  this._state = RunnerState.STARTING;
6707
- await this.worker.start();
7198
+ await this._traces.$(`vitest.${this.worker.name}.start`, { context: this._otel?.workerContext }, () => this.worker.start());
6708
7199
  // Attach event listeners AFTER starting worker to avoid issues
6709
7200
  // if worker.start() fails
6710
7201
  this.worker.on("error", this.emitWorkerError);
6711
7202
  this.worker.on("exit", this.emitUnexpectedExit);
6712
7203
  this.worker.on("message", this.emitWorkerMessage);
7204
+ startSpan = this.startTracesSpan("vitest.worker.start");
6713
7205
  const startPromise = this.withTimeout(this.waitForStart(), START_TIMEOUT);
7206
+ const globalConfig = this.project.vitest.config.experimental.openTelemetry;
7207
+ const projectConfig = this.project.config.experimental.openTelemetry;
7208
+ const tracesEnabled = projectConfig?.enabled ?? globalConfig?.enabled === true;
7209
+ const tracesSdk = projectConfig?.sdkPath ?? globalConfig?.sdkPath;
6714
7210
  this.postMessage({
6715
7211
  type: "start",
6716
7212
  __vitest_worker_request__: true,
@@ -6722,14 +7218,21 @@ class PoolRunner {
6722
7218
  },
6723
7219
  config: this.project.serializedConfig,
6724
7220
  pool: this.worker.name
7221
+ },
7222
+ traces: {
7223
+ enabled: tracesEnabled,
7224
+ sdkPath: tracesSdk,
7225
+ otelCarrier: this.getOTELCarrier()
6725
7226
  }
6726
7227
  });
6727
7228
  await startPromise;
6728
7229
  this._state = RunnerState.STARTED;
6729
7230
  } catch (error) {
6730
7231
  this._state = RunnerState.IDLE;
7232
+ startSpan?.recordException(error);
6731
7233
  throw error;
6732
7234
  } finally {
7235
+ startSpan?.end();
6733
7236
  this._operationLock.resolve();
6734
7237
  this._operationLock = null;
6735
7238
  }
@@ -6738,7 +7241,9 @@ class PoolRunner {
6738
7241
  // Wait for any ongoing operation to complete
6739
7242
  if (this._operationLock) await this._operationLock;
6740
7243
  if (this._state === RunnerState.STOPPED || this._state === RunnerState.STOPPING) return;
7244
+ this._otel?.span.setAttribute("vitest.worker.files", this._otel.files);
6741
7245
  if (this._state === RunnerState.IDLE) {
7246
+ this._otel?.span.end();
6742
7247
  this._state = RunnerState.STOPPED;
6743
7248
  return;
6744
7249
  }
@@ -6748,10 +7253,14 @@ class PoolRunner {
6748
7253
  this._state = RunnerState.STOPPING;
6749
7254
  // Remove exit listener early to avoid "unexpected exit" errors during shutdown
6750
7255
  this.worker.off("exit", this.emitUnexpectedExit);
7256
+ const stopSpan = this.startTracesSpan("vitest.worker.stop");
6751
7257
  await this.withTimeout(new Promise((resolve) => {
6752
7258
  const onStop = (response) => {
6753
7259
  if (response.type === "stopped") {
6754
- if (response.error) this.project.vitest.state.catchError(response.error, "Teardown Error");
7260
+ if (response.error) {
7261
+ stopSpan.recordException(response.error);
7262
+ this.project.vitest.state.catchError(response.error, "Teardown Error");
7263
+ }
6755
7264
  resolve();
6756
7265
  this.off("message", onStop);
6757
7266
  }
@@ -6759,15 +7268,18 @@ class PoolRunner {
6759
7268
  this.on("message", onStop);
6760
7269
  this.postMessage({
6761
7270
  type: "stop",
6762
- __vitest_worker_request__: true
7271
+ __vitest_worker_request__: true,
7272
+ otelCarrier: this.getOTELCarrier()
6763
7273
  });
6764
- }), STOP_TIMEOUT);
7274
+ }), STOP_TIMEOUT).finally(() => {
7275
+ stopSpan.end();
7276
+ });
6765
7277
  this._eventEmitter.removeAllListeners();
6766
7278
  this._offCancel();
6767
7279
  this._rpc.$close(/* @__PURE__ */ new Error("[vitest-pool-runner]: Pending methods while closing rpc"));
6768
7280
  // Stop the worker process (this sets _fork/_thread to undefined)
6769
7281
  // Worker's event listeners (error, message) are implicitly removed when worker terminates
6770
- await this.worker.stop();
7282
+ await this._traces.$(`vitest.${this.worker.name}.stop`, { context: this._otel?.workerContext }, () => this.worker.stop());
6771
7283
  this._state = RunnerState.STOPPED;
6772
7284
  } catch (error) {
6773
7285
  // Ensure we transition to stopped state even on error
@@ -6776,6 +7288,7 @@ class PoolRunner {
6776
7288
  } finally {
6777
7289
  this._operationLock.resolve();
6778
7290
  this._operationLock = null;
7291
+ this._otel?.span.end();
6779
7292
  this._terminatePromise.resolve();
6780
7293
  }
6781
7294
  }
@@ -7231,14 +7744,15 @@ class Pool {
7231
7744
  }
7232
7745
  const poolId = runner.poolId ?? this.getWorkerId();
7233
7746
  runner.poolId = poolId;
7747
+ const span = runner.startTracesSpan(`vitest.worker.${method}`);
7234
7748
  // Start running the test in the worker
7235
- runner.postMessage({
7236
- __vitest_worker_request__: true,
7237
- type: method,
7238
- context: task.context,
7239
- poolId
7749
+ runner.request(method, task.context);
7750
+ await resolver.promise.catch((error) => {
7751
+ span.recordException(error);
7752
+ throw error;
7753
+ }).finally(() => {
7754
+ span.end();
7240
7755
  });
7241
- await resolver.promise;
7242
7756
  const index = this.activeTasks.indexOf(activeTask);
7243
7757
  if (index !== -1) this.activeTasks.splice(index, 1);
7244
7758
  if (!task.isolate && !isMemoryLimitReached && this.queue[0]?.task.isolate === false && isEqualRunner(runner, this.queue[0].task)) {
@@ -7674,7 +8188,8 @@ function serializeConfig(project) {
7674
8188
  standalone: config.standalone,
7675
8189
  printConsoleTrace: config.printConsoleTrace ?? globalConfig.printConsoleTrace,
7676
8190
  benchmark: config.benchmark && { includeSamples: config.benchmark.includeSamples },
7677
- serializedDefines: config.browser.enabled ? "" : project._serializedDefines || ""
8191
+ serializedDefines: config.browser.enabled ? "" : project._serializedDefines || "",
8192
+ experimental: { fsModuleCache: config.experimental.fsModuleCache ?? false }
7678
8193
  };
7679
8194
  }
7680
8195
 
@@ -8725,13 +9240,23 @@ function WorkspaceVitestPlugin(project, options) {
8725
9240
  return project.vitest.matchesProjectFilter(name);
8726
9241
  })) throw new VitestFilteredOutProjectError();
8727
9242
  }
9243
+ const vitestConfig = { name: {
9244
+ label: name,
9245
+ color
9246
+ } };
9247
+ // always inherit the global `fsModuleCache` value even without `extends: true`
9248
+ if (testConfig.experimental?.fsModuleCache == null && project.vitest.config.experimental?.fsModuleCache !== null) {
9249
+ vitestConfig.experimental ??= {};
9250
+ vitestConfig.experimental.fsModuleCache = project.vitest.config.experimental.fsModuleCache;
9251
+ }
9252
+ if (testConfig.experimental?.fsModuleCachePath == null && project.vitest.config.experimental?.fsModuleCachePath !== null) {
9253
+ vitestConfig.experimental ??= {};
9254
+ vitestConfig.experimental.fsModuleCachePath = project.vitest.config.experimental.fsModuleCachePath;
9255
+ }
8728
9256
  return {
8729
9257
  base: "/",
8730
9258
  environments: { __vitest__: { dev: {} } },
8731
- test: { name: {
8732
- label: name,
8733
- color
8734
- } }
9259
+ test: vitestConfig
8735
9260
  };
8736
9261
  }
8737
9262
  },
@@ -8816,16 +9341,21 @@ class VitestResolver {
8816
9341
  options;
8817
9342
  externalizeCache = /* @__PURE__ */ new Map();
8818
9343
  constructor(cacheDir, config) {
9344
+ // sorting to make cache consistent
9345
+ const inline = config.server.deps?.inline;
9346
+ if (Array.isArray(inline)) inline.sort();
9347
+ const external = config.server.deps?.external;
9348
+ if (Array.isArray(external)) external.sort();
8819
9349
  this.options = {
8820
- moduleDirectories: config.deps.moduleDirectories,
9350
+ moduleDirectories: config.deps.moduleDirectories?.sort(),
8821
9351
  inlineFiles: config.setupFiles.flatMap((file) => {
8822
9352
  if (file.startsWith("file://")) return file;
8823
9353
  const resolvedId = resolve(file);
8824
9354
  return [resolvedId, pathToFileURL(resolvedId).href];
8825
9355
  }),
8826
9356
  cacheDir,
8827
- inline: config.server.deps?.inline,
8828
- external: config.server.deps?.external
9357
+ inline,
9358
+ external
8829
9359
  };
8830
9360
  }
8831
9361
  shouldExternalize(file) {
@@ -8882,7 +9412,7 @@ async function isValidNodeImport(id) {
8882
9412
  if (/\.(?:\w+-)?esm?(?:-\w+)?\.js$|\/esm?\//.test(id)) return false;
8883
9413
  try {
8884
9414
  await esModuleLexer.init;
8885
- const code = await promises$1.readFile(id, "utf8");
9415
+ const code = await promises.readFile(id, "utf8");
8886
9416
  const [, , , hasModuleSyntax] = esModuleLexer.parse(code);
8887
9417
  return !hasModuleSyntax;
8888
9418
  } catch {
@@ -9148,15 +9678,24 @@ class TestProject {
9148
9678
  * @param filters String filters to match the test files.
9149
9679
  */
9150
9680
  async globTestFiles(filters = []) {
9151
- const dir = this.config.dir || this.config.root;
9152
- const { include, exclude, includeSource } = this.config;
9153
- const typecheck = this.config.typecheck;
9154
- const [testFiles, typecheckTestFiles] = await Promise.all([typecheck.enabled && typecheck.only ? [] : this.globAllTestFiles(include, exclude, includeSource, dir), typecheck.enabled ? this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir) : []]);
9155
- this.typecheckFilesList = typecheckTestFiles;
9156
- return {
9157
- testFiles: this.filterFiles(testFiles, filters, dir),
9158
- typecheckTestFiles: this.filterFiles(typecheckTestFiles, filters, dir)
9159
- };
9681
+ return this.vitest._traces.$("vitest.config.resolve_include_project", async (span) => {
9682
+ const dir = this.config.dir || this.config.root;
9683
+ const { include, exclude, includeSource } = this.config;
9684
+ const typecheck = this.config.typecheck;
9685
+ span.setAttributes({
9686
+ cwd: dir,
9687
+ include,
9688
+ exclude,
9689
+ includeSource,
9690
+ typecheck: typecheck.enabled ? typecheck.include : []
9691
+ });
9692
+ const [testFiles, typecheckTestFiles] = await Promise.all([typecheck.enabled && typecheck.only ? [] : this.globAllTestFiles(include, exclude, includeSource, dir), typecheck.enabled ? this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir) : []]);
9693
+ this.typecheckFilesList = typecheckTestFiles;
9694
+ return {
9695
+ testFiles: this.filterFiles(testFiles, filters, dir),
9696
+ typecheckTestFiles: this.filterFiles(typecheckTestFiles, filters, dir)
9697
+ };
9698
+ });
9160
9699
  }
9161
9700
  async globAllTestFiles(include, exclude, includeSource, cwd) {
9162
9701
  if (this.testFilesList) return this.testFilesList;
@@ -9165,7 +9704,7 @@ class TestProject {
9165
9704
  const files = await this.globFiles(includeSource, exclude, cwd);
9166
9705
  await Promise.all(files.map(async (file) => {
9167
9706
  try {
9168
- const code = await promises$1.readFile(file, "utf-8");
9707
+ const code = await promises.readFile(file, "utf-8");
9169
9708
  if (this.isInSourceTestCode(code)) testFiles.push(file);
9170
9709
  } catch {
9171
9710
  return null;
@@ -9317,10 +9856,7 @@ class TestProject {
9317
9856
  this._resolver = new VitestResolver(server.config.cacheDir, this._config);
9318
9857
  this._vite = server;
9319
9858
  this._serializedDefines = createDefinesScript(server.config.define);
9320
- this._fetcher = createFetchModuleFunction(this._resolver, this.tmpDir, {
9321
- dumpFolder: this.config.dumpDir,
9322
- readFromDump: this.config.server.debug?.load ?? process.env.VITEST_DEBUG_LOAD_DUMP != null
9323
- });
9859
+ this._fetcher = createFetchModuleFunction(this._resolver, this._config, this.vitest._fsCache, this.vitest._traces, this.tmpDir);
9324
9860
  const environment = server.environments.__vitest__;
9325
9861
  this.runner = new ServerModuleRunner(environment, this._fetcher, this._config);
9326
9862
  }
@@ -9980,6 +10516,14 @@ class TestCase extends ReportedTaskImplementation {
9980
10516
  return [...this.task.annotations];
9981
10517
  }
9982
10518
  /**
10519
+ * @experimental
10520
+ *
10521
+ * Test artifacts recorded via the `recordArtifact` API during the test execution.
10522
+ */
10523
+ artifacts() {
10524
+ return [...this.task.artifacts];
10525
+ }
10526
+ /**
9983
10527
  * Useful information about the test like duration, memory usage, etc.
9984
10528
  * Diagnostic is only available after the test has finished.
9985
10529
  */
@@ -10834,15 +11378,22 @@ class TestRun {
10834
11378
  this.vitest.state.updateUserLog(log);
10835
11379
  await this.vitest.report("onUserConsoleLog", log);
10836
11380
  }
10837
- async annotate(testId, annotation) {
11381
+ async recordArtifact(testId, artifact) {
10838
11382
  const task = this.vitest.state.idMap.get(testId);
10839
11383
  const entity = task && this.vitest.state.getReportedEntity(task);
10840
11384
  assert$1(task && entity, `Entity must be found for task ${task?.name || testId}`);
10841
- assert$1(entity.type === "test", `Annotation can only be added to a test, instead got ${entity.type}`);
10842
- await this.resolveTestAttachment(entity, annotation);
10843
- entity.task.annotations.push(annotation);
10844
- await this.vitest.report("onTestCaseAnnotate", entity, annotation);
10845
- return annotation;
11385
+ assert$1(entity.type === "test", `Artifacts can only be recorded on a test, instead got ${entity.type}`);
11386
+ // annotations won't resolve as artifacts for backwards compatibility until next major
11387
+ if (artifact.type === "internal:annotation") {
11388
+ await this.resolveTestAttachment(entity, artifact.annotation.attachment, artifact.annotation.message);
11389
+ entity.task.annotations.push(artifact.annotation);
11390
+ await this.vitest.report("onTestCaseAnnotate", entity, artifact.annotation);
11391
+ return artifact;
11392
+ }
11393
+ if (Array.isArray(artifact.attachments)) await Promise.all(artifact.attachments.map((attachment) => this.resolveTestAttachment(entity, attachment)));
11394
+ entity.task.artifacts.push(artifact);
11395
+ await this.vitest.report("onTestCaseArtifactRecord", entity, artifact);
11396
+ return artifact;
10846
11397
  }
10847
11398
  async updated(update, events) {
10848
11399
  this.syncUpdateStacks(update);
@@ -10929,15 +11480,14 @@ class TestRun {
10929
11480
  }
10930
11481
  }
10931
11482
  }
10932
- async resolveTestAttachment(test, annotation) {
11483
+ async resolveTestAttachment(test, attachment, filename) {
10933
11484
  const project = test.project;
10934
- const attachment = annotation.attachment;
10935
11485
  if (!attachment) return attachment;
10936
11486
  const path = attachment.path;
10937
11487
  if (path && !path.startsWith("http://") && !path.startsWith("https://")) {
10938
11488
  const currentPath = resolve(project.config.root, path);
10939
11489
  const hash = createHash("sha1").update(currentPath).digest("hex");
10940
- const newPath = resolve(project.config.attachmentsDir, `${sanitizeFilePath(annotation.message)}-${hash}${extname(currentPath)}`);
11490
+ const newPath = resolve(project.config.attachmentsDir, `${filename ? `${sanitizeFilePath(filename)}-` : ""}${hash}${extname(currentPath)}`);
10941
11491
  if (!existsSync(project.config.attachmentsDir)) await mkdir(project.config.attachmentsDir, { recursive: true });
10942
11492
  await copyFile(currentPath, newPath);
10943
11493
  attachment.path = newPath;
@@ -11170,7 +11720,9 @@ class Vitest {
11170
11720
  /** @internal */ _testRun = void 0;
11171
11721
  /** @internal */ _resolver;
11172
11722
  /** @internal */ _fetcher;
11723
+ /** @internal */ _fsCache;
11173
11724
  /** @internal */ _tmpDir = join(tmpdir(), nanoid());
11725
+ /** @internal */ _traces;
11174
11726
  isFirstRun = true;
11175
11727
  restartsCount = 0;
11176
11728
  specifications;
@@ -11249,15 +11801,19 @@ class Vitest {
11249
11801
  const resolved = resolveConfig(this, options, server.config);
11250
11802
  this._config = resolved;
11251
11803
  this._state = new StateManager({ onUnhandledError: resolved.onUnhandledError });
11252
- this._cache = new VitestCache(this.version);
11804
+ this._cache = new VitestCache(this.logger);
11253
11805
  this._snapshot = new SnapshotManager({ ...resolved.snapshotOptions });
11254
11806
  this._testRun = new TestRun(this);
11807
+ const otelSdkPath = resolved.experimental.openTelemetry?.sdkPath;
11808
+ this._traces = new Traces({
11809
+ enabled: !!resolved.experimental.openTelemetry?.enabled,
11810
+ sdkPath: otelSdkPath,
11811
+ watchMode: resolved.watch
11812
+ });
11255
11813
  if (this.config.watch) this.watcher.registerWatcher();
11256
11814
  this._resolver = new VitestResolver(server.config.cacheDir, resolved);
11257
- this._fetcher = createFetchModuleFunction(this._resolver, this._tmpDir, {
11258
- dumpFolder: this.config.dumpDir,
11259
- readFromDump: this.config.server.debug?.load ?? process.env.VITEST_DEBUG_LOAD_DUMP != null
11260
- });
11815
+ this._fsCache = new FileSystemModuleCache(this);
11816
+ this._fetcher = createFetchModuleFunction(this._resolver, this._config, this._fsCache, this._traces, this._tmpDir);
11261
11817
  const environment = server.environments.__vitest__;
11262
11818
  this.runner = new ServerModuleRunner(environment, this._fetcher, resolved);
11263
11819
  if (this.config.watch) {
@@ -11290,7 +11846,8 @@ class Vitest {
11290
11846
  return project.vite.config.getSortedPluginHooks("configureVitest").map((hook) => hook({
11291
11847
  project,
11292
11848
  vitest: this,
11293
- injectTestProjects: this.injectTestProject
11849
+ injectTestProjects: this.injectTestProject,
11850
+ experimental_defineCacheKeyGenerator: (callback) => this._fsCache.defineCacheKeyGenerator(callback)
11294
11851
  }));
11295
11852
  }));
11296
11853
  if (this._cliOptions.browser?.enabled) {
@@ -11308,7 +11865,8 @@ class Vitest {
11308
11865
  if (!this.coreWorkspaceProject) this.coreWorkspaceProject = TestProject._createBasicProject(this);
11309
11866
  if (this.config.testNamePattern) this.configOverride.testNamePattern = this.config.testNamePattern;
11310
11867
  this.reporters = resolved.mode === "benchmark" ? await createBenchmarkReporters(toArray(resolved.benchmark?.reporters), this.runner) : await createReporters(resolved.reporters, this);
11311
- await Promise.all(this._onSetServer.map((fn) => fn()));
11868
+ await this._fsCache.ensureCacheIntegrity();
11869
+ await Promise.all([...this._onSetServer.map((fn) => fn()), this._traces.waitInit()]);
11312
11870
  }
11313
11871
  /** @internal */
11314
11872
  get coverageProvider() {
@@ -11320,10 +11878,19 @@ class Vitest {
11320
11878
  this.configOverride.coverage.enabled = true;
11321
11879
  await this.createCoverageProvider();
11322
11880
  await this.coverageProvider?.onEnabled?.();
11881
+ // onFileTransform is the only thing that affects hash
11882
+ if (this.coverageProvider?.onFileTransform) this.clearAllCachePaths();
11323
11883
  }
11324
11884
  disableCoverage() {
11325
11885
  this.configOverride.coverage ??= {};
11326
11886
  this.configOverride.coverage.enabled = false;
11887
+ // onFileTransform is the only thing that affects hash
11888
+ if (this.coverageProvider?.onFileTransform) this.clearAllCachePaths();
11889
+ }
11890
+ clearAllCachePaths() {
11891
+ this.projects.forEach(({ vite, browser }) => {
11892
+ [...Object.values(vite.environments), ...Object.values(browser?.vite.environments || {})].forEach((environment) => this._fsCache.invalidateAllCachePaths(environment));
11893
+ });
11327
11894
  }
11328
11895
  _coverageOverrideCache = /* @__PURE__ */ new WeakMap();
11329
11896
  /** @internal */
@@ -11419,33 +11986,43 @@ class Vitest {
11419
11986
  return this._coverageProvider;
11420
11987
  }
11421
11988
  /**
11989
+ * Deletes all Vitest caches, including `experimental.fsModuleCache`.
11990
+ * @experimental
11991
+ */
11992
+ async experimental_clearCache() {
11993
+ await this.cache.results.clearCache();
11994
+ await this._fsCache.clearCache();
11995
+ }
11996
+ /**
11422
11997
  * Merge reports from multiple runs located in the specified directory (value from `--merge-reports` if not specified).
11423
11998
  */
11424
11999
  async mergeReports(directory) {
11425
- if (this.reporters.some((r) => r instanceof BlobReporter)) throw new Error("Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.");
11426
- const { files, errors, coverages, executionTimes } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects);
11427
- this.state.blobs = {
11428
- files,
11429
- errors,
11430
- coverages,
11431
- executionTimes
11432
- };
11433
- await this.report("onInit", this);
11434
- const specifications = [];
11435
- for (const file of files) {
11436
- const specification = this.getProjectByName(file.projectName || "").createSpecification(file.filepath, void 0, file.pool);
11437
- specifications.push(specification);
11438
- }
11439
- await this._testRun.start(specifications).catch(noop);
11440
- for (const file of files) await this._reportFileTask(file);
11441
- this._checkUnhandledErrors(errors);
11442
- await this._testRun.end(specifications, errors).catch(noop);
11443
- await this.initCoverageProvider();
11444
- await this.coverageProvider?.mergeReports?.(coverages);
11445
- return {
11446
- testModules: this.state.getTestModules(),
11447
- unhandledErrors: this.state.getUnhandledErrors()
11448
- };
12000
+ return this._traces.$("vitest.merge_reports", async () => {
12001
+ if (this.reporters.some((r) => r instanceof BlobReporter)) throw new Error("Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.");
12002
+ const { files, errors, coverages, executionTimes } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects);
12003
+ this.state.blobs = {
12004
+ files,
12005
+ errors,
12006
+ coverages,
12007
+ executionTimes
12008
+ };
12009
+ await this.report("onInit", this);
12010
+ const specifications = [];
12011
+ for (const file of files) {
12012
+ const specification = this.getProjectByName(file.projectName || "").createSpecification(file.filepath, void 0, file.pool);
12013
+ specifications.push(specification);
12014
+ }
12015
+ await this._testRun.start(specifications).catch(noop);
12016
+ for (const file of files) await this._reportFileTask(file);
12017
+ this._checkUnhandledErrors(errors);
12018
+ await this._testRun.end(specifications, errors).catch(noop);
12019
+ await this.initCoverageProvider();
12020
+ await this.coverageProvider?.mergeReports?.(coverages);
12021
+ return {
12022
+ testModules: this.state.getTestModules(),
12023
+ unhandledErrors: this.state.getUnhandledErrors()
12024
+ };
12025
+ });
11449
12026
  }
11450
12027
  /**
11451
12028
  * Returns the seed, if tests are running in a random order.
@@ -11467,13 +12044,25 @@ class Vitest {
11467
12044
  await this._testRun.updated(packs, events).catch(noop);
11468
12045
  }
11469
12046
  async collect(filters) {
11470
- const files = await this.specifications.getRelevantTestSpecifications(filters);
11471
- // if run with --changed, don't exit if no tests are found
11472
- if (!files.length) return {
11473
- testModules: [],
11474
- unhandledErrors: []
11475
- };
11476
- return this.collectTests(files);
12047
+ return this._traces.$("vitest.collect", async (collectSpan) => {
12048
+ const filenamePattern = filters && filters?.length > 0 ? filters : [];
12049
+ collectSpan.setAttribute("vitest.collect.filters", filenamePattern);
12050
+ const files = await this._traces.$("vitest.config.resolve_include_glob", async () => {
12051
+ const specifications = await this.specifications.getRelevantTestSpecifications(filters);
12052
+ collectSpan.setAttribute("vitest.collect.specifications", specifications.map((s) => {
12053
+ const relativeModuleId = relative(s.project.config.root, s.moduleId);
12054
+ if (s.project.name) return `|${s.project.name}| ${relativeModuleId}`;
12055
+ return relativeModuleId;
12056
+ }));
12057
+ return specifications;
12058
+ });
12059
+ // if run with --changed, don't exit if no tests are found
12060
+ if (!files.length) return {
12061
+ testModules: [],
12062
+ unhandledErrors: []
12063
+ };
12064
+ return this.collectTests(files);
12065
+ });
11477
12066
  }
11478
12067
  /**
11479
12068
  * Returns the list of test files that match the config and filters.
@@ -11491,49 +12080,67 @@ class Vitest {
11491
12080
  * @param filters String filters to match the test files
11492
12081
  */
11493
12082
  async start(filters) {
11494
- try {
11495
- await this.initCoverageProvider();
11496
- await this.coverageProvider?.clean(this._coverageOptions.clean);
11497
- } finally {
11498
- await this.report("onInit", this);
11499
- }
11500
- this.filenamePattern = filters && filters?.length > 0 ? filters : void 0;
11501
- const files = await this.specifications.getRelevantTestSpecifications(filters);
11502
- // if run with --changed, don't exit if no tests are found
11503
- if (!files.length) {
11504
- await this._testRun.start([]);
11505
- const coverage = await this.coverageProvider?.generateCoverage?.({ allTestsRun: true });
11506
- await this._testRun.end([], [], coverage);
11507
- // Report coverage for uncovered files
11508
- await this.reportCoverage(coverage, true);
11509
- if (!this.config.watch || !(this.config.changed || this.config.related?.length)) throw new FilesNotFoundError(this.mode);
11510
- }
11511
- let testModules = {
11512
- testModules: [],
11513
- unhandledErrors: []
11514
- };
11515
- if (files.length) {
11516
- // populate once, update cache on watch
11517
- await this.cache.stats.populateStats(this.config.root, files);
11518
- testModules = await this.runFiles(files, true);
11519
- }
11520
- if (this.config.watch) await this.report("onWatcherStart");
11521
- return testModules;
12083
+ return this._traces.$("vitest.start", async (startSpan) => {
12084
+ startSpan.setAttributes({ config: this.vite.config.configFile });
12085
+ try {
12086
+ await this._traces.$("vitest.coverage.init", async () => {
12087
+ await this.initCoverageProvider();
12088
+ await this.coverageProvider?.clean(this._coverageOptions.clean);
12089
+ });
12090
+ } finally {
12091
+ await this.report("onInit", this);
12092
+ }
12093
+ this.filenamePattern = filters && filters?.length > 0 ? filters : void 0;
12094
+ startSpan.setAttribute("vitest.start.filters", this.filenamePattern || []);
12095
+ const files = await this._traces.$("vitest.config.resolve_include_glob", async () => {
12096
+ const specifications = await this.specifications.getRelevantTestSpecifications(filters);
12097
+ startSpan.setAttribute("vitest.start.specifications", specifications.map((s) => {
12098
+ const relativeModuleId = relative(s.project.config.root, s.moduleId);
12099
+ if (s.project.name) return `|${s.project.name}| ${relativeModuleId}`;
12100
+ return relativeModuleId;
12101
+ }));
12102
+ return specifications;
12103
+ });
12104
+ // if run with --changed, don't exit if no tests are found
12105
+ if (!files.length) {
12106
+ await this._traces.$("vitest.test_run", async () => {
12107
+ await this._testRun.start([]);
12108
+ const coverage = await this.coverageProvider?.generateCoverage?.({ allTestsRun: true });
12109
+ await this._testRun.end([], [], coverage);
12110
+ // Report coverage for uncovered files
12111
+ await this.reportCoverage(coverage, true);
12112
+ });
12113
+ if (!this.config.watch || !(this.config.changed || this.config.related?.length)) throw new FilesNotFoundError(this.mode);
12114
+ }
12115
+ let testModules = {
12116
+ testModules: [],
12117
+ unhandledErrors: []
12118
+ };
12119
+ if (files.length) {
12120
+ // populate once, update cache on watch
12121
+ await this.cache.stats.populateStats(this.config.root, files);
12122
+ testModules = await this.runFiles(files, true);
12123
+ }
12124
+ if (this.config.watch) await this.report("onWatcherStart");
12125
+ return testModules;
12126
+ });
11522
12127
  }
11523
12128
  /**
11524
12129
  * Initialize reporters and the coverage provider. This method doesn't run any tests.
11525
12130
  * If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called.
11526
12131
  */
11527
12132
  async init() {
11528
- try {
11529
- await this.initCoverageProvider();
11530
- await this.coverageProvider?.clean(this._coverageOptions.clean);
11531
- } finally {
11532
- await this.report("onInit", this);
11533
- }
11534
- // populate test files cache so watch mode can trigger a file rerun
11535
- await this.globTestSpecifications();
11536
- if (this.config.watch) await this.report("onWatcherStart");
12133
+ await this._traces.$("vitest.init", async () => {
12134
+ try {
12135
+ await this.initCoverageProvider();
12136
+ await this.coverageProvider?.clean(this._coverageOptions.clean);
12137
+ } finally {
12138
+ await this.report("onInit", this);
12139
+ }
12140
+ // populate test files cache so watch mode can trigger a file rerun
12141
+ await this.globTestSpecifications();
12142
+ if (this.config.watch) await this.report("onWatcherStart");
12143
+ });
11537
12144
  }
11538
12145
  /**
11539
12146
  * If there is a test run happening, returns a promise that will
@@ -11583,51 +12190,53 @@ class Vitest {
11583
12190
  return result;
11584
12191
  }
11585
12192
  async runFiles(specs, allTestsRun) {
11586
- await this._testRun.start(specs);
11587
- // previous run
11588
- await this.cancelPromise;
11589
- await this.runningPromise;
11590
- this._onCancelListeners.clear();
11591
- this.isCancelling = false;
11592
- // schedule the new run
11593
- this.runningPromise = (async () => {
11594
- try {
11595
- if (!this.pool) this.pool = createPool(this);
11596
- const invalidates = Array.from(this.watcher.invalidates);
11597
- this.watcher.invalidates.clear();
11598
- this.snapshot.clear();
11599
- this.state.clearErrors();
11600
- if (!this.isFirstRun && this._coverageOptions.cleanOnRerun) await this.coverageProvider?.clean();
11601
- await this.initializeGlobalSetup(specs);
12193
+ return this._traces.$("vitest.test_run", async () => {
12194
+ await this._testRun.start(specs);
12195
+ // previous run
12196
+ await this.cancelPromise;
12197
+ await this.runningPromise;
12198
+ this._onCancelListeners.clear();
12199
+ this.isCancelling = false;
12200
+ // schedule the new run
12201
+ this.runningPromise = (async () => {
11602
12202
  try {
11603
- await this.pool.runTests(specs, invalidates);
11604
- } catch (err) {
11605
- this.state.catchError(err, "Unhandled Error");
12203
+ if (!this.pool) this.pool = createPool(this);
12204
+ const invalidates = Array.from(this.watcher.invalidates);
12205
+ this.watcher.invalidates.clear();
12206
+ this.snapshot.clear();
12207
+ this.state.clearErrors();
12208
+ if (!this.isFirstRun && this._coverageOptions.cleanOnRerun) await this.coverageProvider?.clean();
12209
+ await this.initializeGlobalSetup(specs);
12210
+ try {
12211
+ await this.pool.runTests(specs, invalidates);
12212
+ } catch (err) {
12213
+ this.state.catchError(err, "Unhandled Error");
12214
+ }
12215
+ const files = this.state.getFiles();
12216
+ this.cache.results.updateResults(files);
12217
+ try {
12218
+ await this.cache.results.writeToCache();
12219
+ } catch {}
12220
+ return {
12221
+ testModules: this.state.getTestModules(),
12222
+ unhandledErrors: this.state.getUnhandledErrors()
12223
+ };
12224
+ } finally {
12225
+ const coverage = await this.coverageProvider?.generateCoverage({ allTestsRun });
12226
+ const errors = this.state.getUnhandledErrors();
12227
+ this._checkUnhandledErrors(errors);
12228
+ await this._testRun.end(specs, errors, coverage);
12229
+ await this.reportCoverage(coverage, allTestsRun);
11606
12230
  }
11607
- const files = this.state.getFiles();
11608
- this.cache.results.updateResults(files);
11609
- try {
11610
- await this.cache.results.writeToCache();
11611
- } catch {}
11612
- return {
11613
- testModules: this.state.getTestModules(),
11614
- unhandledErrors: this.state.getUnhandledErrors()
11615
- };
11616
- } finally {
11617
- const coverage = await this.coverageProvider?.generateCoverage({ allTestsRun });
11618
- const errors = this.state.getUnhandledErrors();
11619
- this._checkUnhandledErrors(errors);
11620
- await this._testRun.end(specs, errors, coverage);
11621
- await this.reportCoverage(coverage, allTestsRun);
11622
- }
11623
- })().finally(() => {
11624
- this.runningPromise = void 0;
11625
- this.isFirstRun = false;
11626
- // all subsequent runs will treat this as a fresh run
11627
- this.config.changed = false;
11628
- this.config.related = void 0;
12231
+ })().finally(() => {
12232
+ this.runningPromise = void 0;
12233
+ this.isFirstRun = false;
12234
+ // all subsequent runs will treat this as a fresh run
12235
+ this.config.changed = false;
12236
+ this.config.related = void 0;
12237
+ });
12238
+ return await this.runningPromise;
11629
12239
  });
11630
- return await this.runningPromise;
11631
12240
  }
11632
12241
  async experimental_parseSpecifications(specifications, options) {
11633
12242
  if (this.mode !== "test") throw new Error(`The \`experimental_parseSpecifications\` does not support "${this.mode}" mode.`);
@@ -11853,8 +12462,14 @@ class Vitest {
11853
12462
  */
11854
12463
  invalidateFile(filepath) {
11855
12464
  this.projects.forEach(({ vite, browser }) => {
11856
- [...Object.values(vite.environments), ...Object.values(browser?.vite.environments || {})].forEach(({ moduleGraph }) => {
11857
- moduleGraph.getModulesByFile(filepath)?.forEach((module) => moduleGraph.invalidateModule(module));
12465
+ [...Object.values(vite.environments), ...Object.values(browser?.vite.environments || {})].forEach((environment) => {
12466
+ const { moduleGraph } = environment;
12467
+ const modules = moduleGraph.getModulesByFile(filepath);
12468
+ if (!modules) return;
12469
+ modules.forEach((module) => {
12470
+ moduleGraph.invalidateModule(module);
12471
+ this._fsCache.invalidateCachePath(environment, module.id);
12472
+ });
11858
12473
  });
11859
12474
  });
11860
12475
  }
@@ -11892,11 +12507,12 @@ class Vitest {
11892
12507
  this.pool = void 0;
11893
12508
  })());
11894
12509
  closePromises.push(...this._onClose.map((fn) => fn()));
11895
- return Promise.allSettled(closePromises).then((results) => {
12510
+ await Promise.allSettled(closePromises).then((results) => {
11896
12511
  results.forEach((r) => {
11897
12512
  if (r.status === "rejected") this.logger.error("error during close", r.reason);
11898
12513
  });
11899
12514
  });
12515
+ await this._traces?.finish();
11900
12516
  })();
11901
12517
  return this.closingPromise;
11902
12518
  }
@@ -12473,7 +13089,8 @@ async function startVitest(mode, cliFilters = [], options = {}, viteOverrides, v
12473
13089
  else ctx.start(cliFilters);
12474
13090
  });
12475
13091
  try {
12476
- if (ctx.config.mergeReports) await ctx.mergeReports();
13092
+ if (ctx.config.clearCache) await ctx.experimental_clearCache();
13093
+ else if (ctx.config.mergeReports) await ctx.mergeReports();
12477
13094
  else if (ctx.config.standalone) await ctx.init();
12478
13095
  else await ctx.start(cliFilters);
12479
13096
  } catch (e) {