@vitest/browser 5.0.0-beta.6 → 5.0.0-beta.7

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.
package/dist/index.js CHANGED
@@ -1,27 +1,24 @@
1
+ import fs, { promises, existsSync, readFileSync, createReadStream } from 'node:fs';
2
+ import { createRequire } from 'node:module';
1
3
  import { ManualMockedModule, RedirectedModule, AutomockedModule, AutospiedModule, MockerRegistry } from '@vitest/mocker';
2
4
  import { dynamicImportPlugin, ServerMockResolver, interceptorPlugin } from '@vitest/mocker/node';
3
- import c from 'tinyrainbow';
4
- import { isFileLoadingAllowed, isValidApiRequest, isFileServingAllowed, distDir, rolldownVersion, resolveApiServerConfig, resolveFsAllow, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
5
- import { fileURLToPath } from 'node:url';
6
- import fs, { readFileSync, createReadStream, promises, existsSync } from 'node:fs';
7
- import { createRequire } from 'node:module';
8
5
  import { distClientRoot } from '@vitest/ui';
9
- import { slash as slash$1, toArray, deepMerge } from '@vitest/utils/helpers';
10
- import MagicString from 'magic-string';
6
+ import { slash as slash$1, deepMerge, cleanUrl as cleanUrl$1, toArray } from '@vitest/utils/helpers';
11
7
  import sirv from 'sirv';
8
+ import c from 'tinyrainbow';
9
+ import { isFileLoadingAllowed, createDebugger, isValidApiRequest, isCSSRequest, distDir, rolldownVersion, isFileServingAllowed } from 'vitest/node';
10
+ import { fileURLToPath } from 'node:url';
12
11
  import crypto from 'node:crypto';
13
- import { platform } from 'node:os';
14
- import 'vite';
12
+ import MagicString from 'magic-string';
15
13
  import { readFile as readFile$1, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
16
14
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
17
15
  import { extractSourcemapFromFile } from '@vitest/utils/source-map/node';
18
16
  import { resolve as resolve$1, basename as basename$1, dirname as dirname$1 } from 'node:path';
17
+ import { platform } from 'node:os';
19
18
  import { PNG } from 'pngjs';
20
19
  import { diff } from '@blazediff/core';
21
20
  import { WebSocketServer } from 'ws';
22
21
 
23
- var version = "5.0.0-beta.6";
24
-
25
22
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
26
23
  function normalizeWindowsPath(input = "") {
27
24
  if (!input) {
@@ -212,11 +209,11 @@ const basename = function(p, extension) {
212
209
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
213
210
  };
214
211
 
212
+ var version = "5.0.0-beta.7";
213
+
215
214
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
216
215
  const distRoot = resolve(pkgRoot, "dist");
217
216
 
218
- const API_TOKEN_FILE = ".vitest-secret-token";
219
-
220
217
  /// <reference types="../types/index.d.ts" />
221
218
 
222
219
  // (c) 2020-present Andrea Giammarchi
@@ -685,7 +682,7 @@ function assertBrowserFileAccess(project, path) {
685
682
  }
686
683
  }
687
684
  function assertBrowserApiWrite(project, path) {
688
- if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
685
+ if (!project.config.api.allowWrite || !project.vitest.config.api.allowWrite) {
689
686
  throw new Error(`Cannot modify file "${path}". File writing is disabled because the server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
690
687
  }
691
688
  }
@@ -816,16 +813,21 @@ async function resolveTester(globalServer, url, res, next) {
816
813
  __VITEST_PROVIDED_CONTEXT__: "{}",
817
814
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
818
815
  });
819
- const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
820
816
  try {
821
- const url = join("/@fs/", browserProject.testerFilepath);
822
- const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml);
817
+ browserProject.testerHtmlTransformed ??= (async () => {
818
+ const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
819
+ const url = join("/@fs/", browserProject.testerFilepath);
820
+ return await browserProject.vite.transformIndexHtml(url, testerHtml);
821
+ })();
822
+ const indexhtml = await browserProject.testerHtmlTransformed;
823
823
  const html = replacer(indexhtml, {
824
824
  __VITEST_FAVICON__: globalServer.faviconUrl,
825
825
  __VITEST_INJECTOR__: injector
826
826
  });
827
827
  return html;
828
828
  } catch (err) {
829
+ // don't cache a rejection — the next request should retry the transform
830
+ browserProject.testerHtmlTransformed = undefined;
829
831
  session.fail(err);
830
832
  next(err);
831
833
  }
@@ -852,49 +854,41 @@ function createTesterMiddleware(browserServer) {
852
854
 
853
855
  const VIRTUAL_ID_CONTEXT = "\0vitest/browser";
854
856
  const ID_CONTEXT = "vitest/browser";
855
- // for libraries that use an older import but are not type checked
856
- const DEPRECATED_ID_CONTEXT = "@vitest/browser/context";
857
- const DEPRECATED_VIRTUAL_ID_UTILS = "\0@vitest/browser/utils";
858
- const DEPRECATED_ID_UTILS = "@vitest/browser/utils";
859
857
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
860
- function BrowserContext(globalServer) {
858
+ function BrowserContext(contribution) {
861
859
  return {
862
860
  name: "vitest:browser:virtual-module:context",
863
861
  enforce: "pre",
864
- resolveId(id, importer) {
865
- if (id === ID_CONTEXT) {
866
- return VIRTUAL_ID_CONTEXT;
867
- }
868
- if (id === DEPRECATED_ID_CONTEXT) {
869
- if (importer && !importer.includes("/node_modules/")) {
870
- globalServer.vitest.logger.deprecate(`${importer} tries to load a deprecated "${id}" module. ` + `This import will stop working in the next major version. ` + `Please, use "vitest/browser" instead.`);
862
+ resolveId: {
863
+ order: "pre",
864
+ handler(id) {
865
+ if (id === ID_CONTEXT) {
866
+ return VIRTUAL_ID_CONTEXT;
871
867
  }
872
- return VIRTUAL_ID_CONTEXT;
873
- }
874
- if (id === DEPRECATED_ID_UTILS) {
875
- return DEPRECATED_VIRTUAL_ID_UTILS;
876
868
  }
877
869
  },
878
870
  load(id) {
871
+ const globalServer = contribution.parent;
879
872
  if (id === VIRTUAL_ID_CONTEXT) {
880
873
  return generateContextFile.call(this, globalServer);
881
874
  }
882
- if (id === DEPRECATED_VIRTUAL_ID_UTILS) {
883
- return `
884
- import { utils } from 'vitest/browser'
885
- export const getElementLocatorSelectors = utils.getElementLocatorSelectors
886
- export const debug = utils.debug
887
- export const prettyDOM = utils.prettyDOM
888
- export const getElementError = utils.getElementError
889
- `;
890
- }
891
875
  }
892
876
  };
893
877
  }
894
878
  async function generateContextFile(globalServer) {
895
879
  const commands = Object.keys(globalServer.commands);
896
- const provider = [...globalServer.children][0].provider;
897
- const providerName = provider?.name || "preview";
880
+ // The provider instance is initialized lazily when a page opens, so reading
881
+ // `child.provider` here races and can be `undefined` before the first page is
882
+ // opened. The provider name is uniform across a project's instances and is
883
+ // already resolved on the config, so read it from there for this shared
884
+ // (cached) context module. The instance is still used below for the
885
+ // preview-only user-event import.
886
+ const child = [...globalServer.children][0];
887
+ const provider = child.provider;
888
+ const providerName = child.config.browser.provider?.name;
889
+ if (!providerName) {
890
+ throw new Error(`Browser provider is not defined for the project "${child.project.name}". This is a bug in Vitest. Please, open a new issue with a reproduction.`);
891
+ }
898
892
  const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
899
893
  return ` ["${command}"]: (...args) => __vitest_browser_runner__.commands.triggerCommand("${command}", args),`;
900
894
  }).join("\n");
@@ -929,256 +923,41 @@ async function getUserEventImport(provider, resolve) {
929
923
  throw new Error(`Failed to resolve user-event package from ${previewDistRoot}`);
930
924
  }
931
925
  return `\
932
- import { userEvent as __vitest_user_event__ } from '${slash$1(`/@fs/${resolved.id}`)}'
933
- const _userEventSetup = __vitest_user_event__
926
+ import { userEvent as _userEventSetup } from '${slash$1(`/@fs/${resolved.id}`)}'
934
927
  `;
935
928
  }
936
929
 
937
- const versionRegexp = /(?:\?|&)v=\w{8}/;
938
- var BrowserPlugin = (parentServer, base = "/") => {
939
- function isPackageExists(pkg, root) {
940
- return parentServer.vitest.packageInstaller.isPackageExists?.(pkg, { paths: [root] });
941
- }
930
+ var BrowserPlugin = (contribution) => {
942
931
  return [
943
- {
944
- enforce: "pre",
945
- name: "vitest:browser",
946
- async configureServer(server) {
947
- parentServer.setServer(server);
948
- // eslint-disable-next-line prefer-arrow-callback
949
- server.middlewares.use(function vitestHeaders(_req, res, next) {
950
- const headers = server.config.server.headers;
951
- if (headers) {
952
- for (const name in headers) {
953
- res.setHeader(name, headers[name]);
954
- }
955
- }
956
- next();
957
- });
958
- // strip _vitest_original query added by importActual so that
959
- // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
960
- server.middlewares.use((req, _res, next) => {
961
- if (req.url?.includes("_vitest_original") && parentServer.project.config.browser.provider?.name === "playwright") {
962
- req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
963
- }
964
- next();
965
- });
966
- server.middlewares.use(createOrchestratorMiddleware(parentServer));
967
- server.middlewares.use(createTesterMiddleware(parentServer));
968
- server.middlewares.use(`${base}favicon.svg`, (_, res) => {
969
- const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
970
- res.write(content, "utf-8");
971
- res.end();
972
- });
973
- // Serve coverage HTML at ./coverage if configured
974
- const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
975
- if (coverageHtmlDir) {
976
- server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
977
- single: true,
978
- dev: true,
979
- setHeaders: (res) => {
980
- const csp = res.getHeader("Content-Security-Policy");
981
- if (typeof csp === "string") {
982
- // add frame-ancestors to allow the iframe to be loaded by Vitest,
983
- // but keep the rest of the CSP
984
- res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
985
- }
986
- res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
987
- }
988
- }));
989
- }
990
- server.middlewares.use((req, res, next) => {
991
- // 9000 mega head move
992
- // Vite always caches optimized dependencies, but users might mock
993
- // them in _some_ tests, while keeping original modules in others
994
- // there is no way to configure that in Vite, so we patch it here
995
- // to always ignore the cache-control set by Vite in the next middleware
996
- if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
997
- res.setHeader("Cache-Control", "no-cache");
998
- const setHeader = res.setHeader.bind(res);
999
- res.setHeader = function(name, value) {
1000
- if (name === "Cache-Control") {
1001
- return res;
1002
- }
1003
- return setHeader(name, value);
1004
- };
1005
- }
1006
- next();
1007
- });
1008
- // handle attachments the same way as in packages/ui/node/index.ts
1009
- server.middlewares.use((req, res, next) => {
1010
- if (!req.url) {
1011
- return next();
1012
- }
1013
- const url = new URL(req.url, "http://localhost");
1014
- if (url.pathname !== "/__vitest_attachment__") {
1015
- return next();
1016
- }
1017
- const path = url.searchParams.get("path");
1018
- const contentType = url.searchParams.get("contentType");
1019
- if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
1020
- return next();
1021
- }
1022
- const fsPath = decodeURIComponent(path);
1023
- if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
1024
- return next();
1025
- }
1026
- try {
1027
- res.setHeader("content-type", contentType);
1028
- return createReadStream(fsPath).pipe(res).on("close", () => res.end());
1029
- } catch (err) {
1030
- return next(err);
1031
- }
1032
- });
1033
- }
1034
- },
1035
932
  {
1036
933
  name: "vitest:browser:tests",
1037
934
  enforce: "pre",
1038
- async config() {
1039
- // this plugin can be used in different projects, but all of them
1040
- // have the same `include` pattern, so it doesn't matter which project we use
1041
- const project = parentServer.project;
1042
- // only glob benchmarks when a browser-enabled bench project exists in
1043
- // the workspace — keeps the optimizeDeps entries symmetrical across
1044
- // the test/bench project clones so the optimizer doesn't re-scan when
1045
- // the user switches modes
1046
- const hasBrowserBenchProject = parentServer.vitest.projects.some((p) => p.config.browser.enabled && p.config.benchmark.enabled);
1047
- const benchInclude = hasBrowserBenchProject ? project.config.benchmark.include : [];
1048
- const dir = project.config.dir || project.config.root;
1049
- const [{ testFiles: browserTestFiles }, browserBenchFiles] = await Promise.all([project.globTestFiles(), benchInclude.length > 0 ? project.globFiles(benchInclude, project.config.benchmark.exclude ?? project.config.exclude, dir) : []]);
1050
- const setupFiles = toArray(project.config.setupFiles);
1051
- // replace env values - cannot be reassign at runtime
1052
- const define = {};
1053
- for (const env in project.config.env || {}) {
1054
- const stringValue = JSON.stringify(project.config.env[env]);
1055
- define[`import.meta.env.${env}`] = stringValue;
1056
- }
1057
- const entries = [
1058
- ...new Set([...browserTestFiles, ...browserBenchFiles]),
1059
- ...setupFiles,
1060
- resolve(distDir, "index.js"),
1061
- resolve(distDir, "browser.js"),
1062
- resolve(distDir, "runners.js"),
1063
- resolve(distDir, "utils.js"),
1064
- ...project.config.snapshotSerializers || []
1065
- ];
1066
- const exclude = [
1067
- "vitest",
1068
- "vitest/browser",
1069
- "vitest/internal/browser",
1070
- "vite/module-runner",
1071
- "@vitest/browser/utils",
1072
- "@vitest/browser/context",
1073
- "@vitest/browser/client",
1074
- "@vitest/utils",
1075
- "@vitest/utils/source-map",
1076
- "@vitest/spy",
1077
- "@vitest/utils/error",
1078
- "std-env",
1079
- "tinybench",
1080
- "tinyspy",
1081
- "tinyrainbow",
1082
- "pathe",
1083
- "msw",
1084
- "msw/browser"
1085
- ];
1086
- if (typeof project.config.diff === "string") {
1087
- entries.push(project.config.diff);
1088
- }
1089
- if (parentServer.vitest.coverageProvider) {
1090
- const coverage = parentServer.vitest._coverageOptions;
1091
- const provider = coverage.provider;
1092
- if (provider === "v8") {
1093
- const path = tryResolve("@vitest/coverage-v8", [parentServer.config.root]);
1094
- if (path) {
1095
- entries.push(path);
1096
- exclude.push("@vitest/coverage-v8/browser");
1097
- }
1098
- } else if (provider === "istanbul") {
1099
- const path = tryResolve("@vitest/coverage-istanbul", [parentServer.config.root]);
1100
- if (path) {
1101
- entries.push(path);
1102
- exclude.push("@vitest/coverage-istanbul");
1103
- }
1104
- } else if (provider === "custom" && coverage.customProviderModule) {
1105
- entries.push(coverage.customProviderModule);
935
+ resolveId: {
936
+ order: "pre",
937
+ handler(id) {
938
+ if (!/\?browserv=\w+$/.test(id)) {
939
+ return;
1106
940
  }
1107
- }
1108
- const include = [
1109
- "vitest > expect-type",
1110
- "vitest > magic-string",
1111
- "vitest > chai"
1112
- ];
1113
- const provider = parentServer.config.browser.provider || [...parentServer.children][0]?.provider;
1114
- if (provider?.name === "preview") {
1115
- include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
1116
- }
1117
- const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root;
1118
- const svelte = isPackageExists("vitest-browser-svelte", fileRoot);
1119
- if (svelte) {
1120
- exclude.push("vitest-browser-svelte");
1121
- }
1122
- // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
1123
- const vue = isPackageExists("vitest-browser-vue", fileRoot);
1124
- if (vue) {
1125
- // we override them in the esbuild plugin so optimizer can no longer intercept it
1126
- include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
1127
- }
1128
- const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
1129
- if (vueTestUtils) {
1130
- include.push("@vue/test-utils");
1131
- }
1132
- const otelConfig = project.config.experimental.openTelemetry;
1133
- if (otelConfig?.enabled && otelConfig.browserSdkPath) {
1134
- entries.push(otelConfig.browserSdkPath);
1135
- include.push("@opentelemetry/api");
1136
- } else {
1137
- exclude.push("@opentelemetry/api");
1138
- }
1139
- return {
1140
- define,
1141
- resolve: { dedupe: ["vitest"] },
1142
- optimizeDeps: {
1143
- entries,
1144
- exclude,
1145
- include
941
+ let useId = id.slice(0, id.lastIndexOf("?"));
942
+ if (useId.startsWith("/@fs/")) {
943
+ useId = useId.slice(5);
1146
944
  }
1147
- };
1148
- },
1149
- async resolveId(id) {
1150
- if (!/\?browserv=\w+$/.test(id)) {
1151
- return;
1152
- }
1153
- let useId = id.slice(0, id.lastIndexOf("?"));
1154
- if (useId.startsWith("/@fs/")) {
1155
- useId = useId.slice(5);
1156
- }
1157
- if (/^\w:/.test(useId)) {
1158
- useId = useId.replace(/\\/g, "/");
1159
- }
1160
- return useId;
1161
- }
1162
- },
1163
- {
1164
- name: "vitest:browser:resolve-virtual",
1165
- async resolveId(rawId) {
1166
- if (rawId === "/mockServiceWorker.js") {
1167
- return this.resolve("msw/mockServiceWorker.js", distRoot, { skipSelf: true });
945
+ if (/^\w:/.test(useId)) {
946
+ useId = useId.replace(/\\/g, "/");
947
+ }
948
+ return useId;
1168
949
  }
1169
950
  }
1170
951
  },
1171
952
  {
1172
953
  name: "vitest:browser:assets",
1173
- configureServer(server) {
1174
- server.middlewares.use("/__vitest__", sirv(distClientRoot));
1175
- },
1176
954
  resolveId(id) {
1177
955
  if (id.startsWith("/__vitest_browser__/")) {
1178
956
  return resolve(distRoot, "client", id.slice(1));
1179
957
  }
1180
958
  },
1181
959
  transform(code, id) {
960
+ const parentServer = contribution.parent;
1182
961
  if (id.includes(parentServer.vite.config.cacheDir) && id.includes("loupe.js")) {
1183
962
  // loupe bundle has a nasty require('util') call that leaves a warning in the console
1184
963
  const utilRequire = "nodeUtil = require_util();";
@@ -1186,47 +965,16 @@ var BrowserPlugin = (parentServer, base = "/") => {
1186
965
  }
1187
966
  }
1188
967
  },
1189
- BrowserContext(parentServer),
968
+ BrowserContext(contribution),
1190
969
  dynamicImportPlugin({
1191
970
  globalThisAccessor: "\"__vitest_browser_runner__\"",
1192
- filter(id, environment) {
1193
- if (environment.name !== "client") {
1194
- return false;
1195
- }
971
+ filter(id) {
1196
972
  if (id.includes(distRoot)) {
1197
973
  return false;
1198
974
  }
1199
975
  return true;
1200
976
  }
1201
977
  }),
1202
- {
1203
- name: "vitest:browser:config",
1204
- enforce: "post",
1205
- async config(viteConfig) {
1206
- // Enables using ignore hint for coverage providers with @preserve keyword
1207
- // Only set esbuild options when not using rolldown-vite (Vite 8+),
1208
- // which uses oxc for transformation instead of esbuild
1209
- if (!rolldownVersion && viteConfig.esbuild !== false) {
1210
- viteConfig.esbuild ||= {};
1211
- viteConfig.esbuild.legalComments = "inline";
1212
- }
1213
- const defaultPort = parentServer.vitest.state._data.browserLastPort++;
1214
- const api = resolveApiServerConfig(viteConfig.test?.browser || {}, defaultPort);
1215
- viteConfig.server = {
1216
- ...viteConfig.server,
1217
- port: defaultPort,
1218
- ...api,
1219
- middlewareMode: false,
1220
- open: false
1221
- };
1222
- viteConfig.server.fs ??= {};
1223
- viteConfig.server.fs.allow = viteConfig.server.fs.allow || [];
1224
- viteConfig.server.fs.deny ??= [];
1225
- viteConfig.server.fs.deny.push(API_TOKEN_FILE);
1226
- viteConfig.server.fs.allow.push(...resolveFsAllow(parentServer.vitest.config.root, parentServer.vitest.vite.config.configFile), distRoot);
1227
- return { resolve: { alias: viteConfig.test?.alias } };
1228
- }
1229
- },
1230
978
  {
1231
979
  name: "vitest:browser:in-source-tests",
1232
980
  transform: {
@@ -1259,130 +1007,6 @@ var BrowserPlugin = (parentServer, base = "/") => {
1259
1007
  }
1260
1008
  }
1261
1009
  },
1262
- {
1263
- name: "vitest:browser:transform-tester-html",
1264
- enforce: "pre",
1265
- async transformIndexHtml(html, ctx) {
1266
- const projectBrowser = [...parentServer.children].find((server) => {
1267
- return ctx.filename === server.testerFilepath;
1268
- });
1269
- if (!projectBrowser) {
1270
- return;
1271
- }
1272
- const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
1273
- const testerTags = [];
1274
- const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
1275
- if (!isDefaultTemplate) {
1276
- const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
1277
- const testerEntry = manifestContent["tester/tester.html"];
1278
- testerTags.push({
1279
- tag: "script",
1280
- attrs: {
1281
- type: "module",
1282
- crossorigin: "",
1283
- src: `${parentServer.base}${testerEntry.file}`
1284
- },
1285
- injectTo: "head"
1286
- });
1287
- for (const importName of testerEntry.imports || []) {
1288
- const entryManifest = manifestContent[importName];
1289
- if (entryManifest) {
1290
- testerTags.push({
1291
- tag: "link",
1292
- attrs: {
1293
- href: `${parentServer.base}${entryManifest.file}`,
1294
- rel: "modulepreload",
1295
- crossorigin: ""
1296
- },
1297
- injectTo: "head"
1298
- });
1299
- }
1300
- }
1301
- } else {
1302
- // inject the reset style only in the default template,
1303
- // allowing users to customize the style in their own template
1304
- testerTags.push({
1305
- tag: "style",
1306
- children: `
1307
- html {
1308
- padding: 0;
1309
- margin: 0;
1310
- }
1311
- body {
1312
- padding: 0;
1313
- margin: 0;
1314
- min-height: 100vh;
1315
- }`,
1316
- injectTo: "head"
1317
- });
1318
- }
1319
- return [
1320
- {
1321
- tag: "script",
1322
- children: "{__VITEST_INJECTOR__}",
1323
- injectTo: "head-prepend"
1324
- },
1325
- {
1326
- tag: "script",
1327
- children: stateJs,
1328
- injectTo: "head-prepend"
1329
- },
1330
- {
1331
- tag: "script",
1332
- attrs: {
1333
- type: "module",
1334
- src: parentServer.errorCatcherUrl
1335
- },
1336
- injectTo: "head"
1337
- },
1338
- {
1339
- tag: "script",
1340
- attrs: {
1341
- type: "module",
1342
- src: parentServer.matchersUrl
1343
- },
1344
- injectTo: "head"
1345
- },
1346
- ...parentServer.initScripts.map((script) => ({
1347
- tag: "script",
1348
- attrs: {
1349
- type: "module",
1350
- src: join("/@fs/", script)
1351
- },
1352
- injectTo: "head"
1353
- })),
1354
- ...testerTags
1355
- ].filter((s) => s != null);
1356
- }
1357
- },
1358
- {
1359
- name: "vitest:browser:support-testing-library",
1360
- enforce: "pre",
1361
- config() {
1362
- const rolldownPlugin = {
1363
- name: "vue-test-utils-rewrite",
1364
- resolveId: {
1365
- filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
1366
- handler(source, importer) {
1367
- const resolved = getRequire().resolve(source, { paths: [importer] });
1368
- return resolved;
1369
- }
1370
- }
1371
- };
1372
- const esbuildPlugin = {
1373
- name: "test-utils-rewrite",
1374
- setup(build) {
1375
- // test-utils: resolve to CJS instead of the browser because the browser version expects a global Vue object
1376
- // compiler-core: only CJS version allows slots as strings
1377
- build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
1378
- const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
1379
- return { path: resolved };
1380
- });
1381
- }
1382
- };
1383
- return { optimizeDeps: rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } } };
1384
- }
1385
- },
1386
1010
  {
1387
1011
  name: "vitest:browser:__vitest_browser_import_meta_env_init__",
1388
1012
  transform: { handler(code) {
@@ -1395,21 +1019,6 @@ body {
1395
1019
  }
1396
1020
  ];
1397
1021
  };
1398
- function tryResolve(path, paths) {
1399
- try {
1400
- const _require = getRequire();
1401
- return _require.resolve(path, { paths });
1402
- } catch {
1403
- return undefined;
1404
- }
1405
- }
1406
- let _require;
1407
- function getRequire() {
1408
- if (!_require) {
1409
- _require = createRequire(import.meta.url);
1410
- }
1411
- return _require;
1412
- }
1413
1022
  const postfixRE = /[?#].*$/;
1414
1023
  function cleanUrl(url) {
1415
1024
  return url.replace(postfixRE, "");
@@ -1455,19 +1064,6 @@ class BrowserServerCDPHandler {
1455
1064
  }
1456
1065
  }
1457
1066
 
1458
- const _startV8Coverage = async (context) => {
1459
- const session = await context.__ensureCDPHandler();
1460
- await session.send("Profiler.enable");
1461
- await session.send("Profiler.startPreciseCoverage", {
1462
- callCount: true,
1463
- detailed: true
1464
- });
1465
- };
1466
- const _takeV8Coverage = async (context) => {
1467
- const session = await context.__ensureCDPHandler();
1468
- return session.send("Profiler.takePreciseCoverage");
1469
- };
1470
-
1471
1067
  const types = {
1472
1068
  'application/andrew-inset': ['ez'],
1473
1069
  'application/appinstaller': ['appinstaller'],
@@ -2606,8 +2202,6 @@ var builtinCommands = {
2606
2202
  readFile,
2607
2203
  removeFile,
2608
2204
  writeFile,
2609
- __vitest_startV8Coverage: _startV8Coverage,
2610
- __vitest_takeV8Coverage: _takeV8Coverage,
2611
2205
  __vitest_markTrace: _markTrace,
2612
2206
  __vitest_groupTraceStart: _groupTraceStart,
2613
2207
  __vitest_groupTraceEnd: _groupTraceEnd,
@@ -2625,6 +2219,10 @@ class BrowserServerState {
2625
2219
  class ProjectBrowser {
2626
2220
  testerHtml;
2627
2221
  testerFilepath;
2222
+ // the tester template is read once per server, so the transformed HTML
2223
+ // is stable too — computing it for every iframe request pays the whole
2224
+ // transformIndexHtml plugin pipeline each time
2225
+ testerHtmlTransformed;
2628
2226
  provider;
2629
2227
  vitest;
2630
2228
  vite;
@@ -2726,21 +2324,27 @@ class ParentBrowserProject {
2726
2324
  config;
2727
2325
  // cache for non-vite source maps
2728
2326
  sourceMapCache = new Map();
2729
- constructor(project, base) {
2730
- this.project = project;
2327
+ constructor(ctx, base) {
2731
2328
  this.base = base;
2732
- this.vitest = project.vitest;
2733
- this.config = project.config;
2329
+ this.vitest = ctx.vitest;
2330
+ this.config = ctx.config;
2734
2331
  this.stackTraceOptions = {
2735
- frameFilter: project.config.onStackTrace,
2332
+ frameFilter: this.config.onStackTrace,
2736
2333
  getSourceMap: (id) => {
2737
2334
  if (this.sourceMapCache.has(id)) {
2738
2335
  return this.sourceMapCache.get(id);
2739
2336
  }
2740
2337
  const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
2741
- // handle non-inline source map such as pre-bundled deps in node_modules/.vite
2742
- if (result && !result.map) {
2743
- const filePath = id.split("?")[0];
2338
+ const filePath = id.split("?")[0];
2339
+ // prefer the map stored on disk when the transform pipeline can't
2340
+ // provide a usable one:
2341
+ // - pre-bundled deps: the pipeline map resolves back into the
2342
+ // optimizer cache, while the map esbuild wrote next to the file
2343
+ // points at the real package sources
2344
+ // - an empty `mappings` means the map was intentionally not served
2345
+ // to the browser (`vitest:browser:framework-sourcemaps`), but disk
2346
+ // is still the source of truth for error stack traces
2347
+ if (result && (!result.map || result.map.mappings === "" || filePath.startsWith(this.vite.config.cacheDir))) {
2744
2348
  const extracted = extractSourcemapFromFile(result.code, filePath);
2745
2349
  this.sourceMapCache.set(id, extracted?.map);
2746
2350
  return extracted?.map;
@@ -2771,11 +2375,11 @@ class ParentBrowserProject {
2771
2375
  this.commands[name] ??= command;
2772
2376
  }
2773
2377
  // validate names because they can't be used as identifiers
2774
- for (const command in project.config.browser.commands) {
2378
+ for (const command in this.config.browser.commands) {
2775
2379
  if (!/^[a-z_$][\w$]*$/i.test(command)) {
2776
2380
  throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
2777
2381
  }
2778
- this.commands[command] = project.config.browser.commands[command];
2382
+ this.commands[command] = this.config.browser.commands[command];
2779
2383
  }
2780
2384
  this.prefixTesterUrl = `${base || "/"}`;
2781
2385
  this.prefixOrchestratorUrl = `${base}__vitest_test__/`;
@@ -2783,7 +2387,7 @@ class ParentBrowserProject {
2783
2387
  this.manifest = (async () => {
2784
2388
  return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
2785
2389
  })().then((manifest) => this.manifest = manifest);
2786
- this.orchestratorHtml = (project.config.browser.ui ? readFile$1(resolve(distClientRoot, "index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
2390
+ this.orchestratorHtml = (this.config.browser.ui ? readFile$1(resolve(distClientRoot, "index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
2787
2391
  this.injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8").then((js) => this.injectorJs = js);
2788
2392
  this.errorCatcherUrl = join("/@fs/", resolve(distRoot, "client/error-catcher.js"));
2789
2393
  this.matchersUrl = join("/@fs/", distRoot, "expect-element.js");
@@ -3161,14 +2765,14 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3161
2765
  }
3162
2766
  }
3163
2767
  function canWrite(project) {
3164
- return project.config.browser.api.allowWrite && project.vitest.config.browser.api.allowWrite && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
2768
+ return project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3165
2769
  }
3166
2770
  function isCdpAllowed(project) {
3167
- return project.config.api.allowExec && project.config.browser.api.allowExec && project.vitest.config.api.allowExec && project.vitest.config.browser.api.allowExec && project.config.api.allowWrite && project.config.browser.api.allowWrite && project.vitest.config.api.allowWrite && project.vitest.config.browser.api.allowWrite;
2771
+ return project.config.api.allowExec && project.vitest.config.api.allowExec && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3168
2772
  }
3169
2773
  function assertCdpAllowed(project) {
3170
2774
  if (!isCdpAllowed(project)) {
3171
- throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`);
2775
+ throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/api.`);
3172
2776
  }
3173
2777
  }
3174
2778
  function setupClient(project, rpcId, ws, options) {
@@ -3204,13 +2808,13 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3204
2808
  if (!canWrite(project)) {
3205
2809
  if (artifact.type === "internal:annotation" && artifact.annotation.attachment) {
3206
2810
  artifact.annotation.attachment = undefined;
3207
- vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2811
+ vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/api.`);
3208
2812
  }
3209
2813
  // remove attachments if cannot write
3210
2814
  if (artifact.attachments?.length) {
3211
2815
  const attachments = artifact.attachments.map((n) => n.path).filter((r) => !!r).join("\", \"");
3212
2816
  artifact.attachments = [];
3213
- vitest.logger.error(`[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/browser/api.`);
2817
+ vitest.logger.error(`[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/api.`);
3214
2818
  }
3215
2819
  } else {
3216
2820
  // attachment files are copied into `attachmentsDir`, so confine
@@ -3234,7 +2838,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3234
2838
  },
3235
2839
  async writeBenchmarkResult(relativePath, data) {
3236
2840
  if (!canWrite(project)) {
3237
- vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2841
+ vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/api.`);
3238
2842
  return;
3239
2843
  }
3240
2844
  checkFileAccess(project.benchmark.resolve(relativePath));
@@ -3276,7 +2880,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3276
2880
  async saveSnapshotFile(id, content) {
3277
2881
  checkFileAccess(id);
3278
2882
  if (!canWrite(project)) {
3279
- vitest.logger.error(`[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
2883
+ vitest.logger.error(`[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`);
3280
2884
  return;
3281
2885
  }
3282
2886
  await promises.mkdir(dirname(id), { recursive: true });
@@ -3285,7 +2889,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3285
2889
  async removeSnapshotFile(id) {
3286
2890
  checkFileAccess(id);
3287
2891
  if (!canWrite(project)) {
3288
- vitest.logger.error(`[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
2892
+ vitest.logger.error(`[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`);
3289
2893
  return;
3290
2894
  }
3291
2895
  if (!existsSync(id)) {
@@ -3466,66 +3070,437 @@ function stringifyReplace(key, value) {
3466
3070
  function defineBrowserCommand(fn) {
3467
3071
  return fn;
3468
3072
  }
3469
- const createBrowserServer = async (options) => {
3470
- const project = options.project;
3471
- const configFile = project.vite.config.configFile;
3472
- if (project.vitest.version !== version) {
3473
- project.vitest.logger.warn(c.yellow(`Loaded ${c.inverse(c.yellow(` vitest@${project.vitest.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + "\nRunning mixed versions is not supported and may lead into bugs" + "\nUpdate your dependencies and make sure the versions match."));
3474
- }
3475
- const server = new ParentBrowserProject(project, "/");
3476
- const configPath = typeof configFile === "string" ? configFile : false;
3477
- const logLevel = process.env.VITEST_BROWSER_DEBUG ?? "info";
3478
- const logger = createViteLogger(project.vitest.logger, logLevel, { allowClearScreen: false });
3073
+ const versionRegexp = /(?:\?|&)v=\w{8}/;
3074
+ /**
3075
+ * The browser provider's `serverFactory`. Returns a `BrowserServerContribution`
3076
+ * that Vitest core uses to build the SINGLE Vite server shared by `project.vite`
3077
+ * and `project.browser.vite`. This factory does NOT create a server (core does);
3078
+ * it only contributes config, plugins, the parent factory, and the RPC setup.
3079
+ */
3080
+ const createBrowserServer = async () => {
3479
3081
  const mockerRegistry = new MockerRegistry();
3480
- let cacheDir;
3481
- const vite = await createViteServer({
3482
- ...project.options,
3483
- define: project.config.viteDefine,
3484
- base: "/",
3485
- root: project.config.root,
3486
- logLevel,
3487
- customLogger: {
3488
- ...logger,
3489
- info(msg, options) {
3490
- logger.info(msg, options);
3491
- if (msg.includes("optimized dependencies changed. reloading")) {
3492
- logger.warn([c.yellow(`\n${c.bold("[vitest]")} Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs.\n`), c.yellow(`For a stable experience, please add mentioned dependencies to your config\'s ${c.bold("`optimizeDeps.include`")} field manually.\n\n`)].join(""));
3082
+ const contribution = {
3083
+ async transformIndexHtml(ctx) {
3084
+ const parentServer = contribution.parent;
3085
+ const projectBrowser = [...parentServer.children].find((server) => {
3086
+ return ctx.filename === server.testerFilepath;
3087
+ });
3088
+ if (!projectBrowser) {
3089
+ return;
3090
+ }
3091
+ const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
3092
+ const testerTags = [];
3093
+ const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
3094
+ if (!isDefaultTemplate) {
3095
+ const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
3096
+ const testerEntry = manifestContent["tester/tester.html"];
3097
+ testerTags.push({
3098
+ tag: "script",
3099
+ attrs: {
3100
+ type: "module",
3101
+ crossorigin: "",
3102
+ src: `${parentServer.base}${testerEntry.file}`
3103
+ },
3104
+ injectTo: "head"
3105
+ });
3106
+ for (const importName of testerEntry.imports || []) {
3107
+ const entryManifest = manifestContent[importName];
3108
+ if (entryManifest) {
3109
+ testerTags.push({
3110
+ tag: "link",
3111
+ attrs: {
3112
+ href: `${parentServer.base}${entryManifest.file}`,
3113
+ rel: "modulepreload",
3114
+ crossorigin: ""
3115
+ },
3116
+ injectTo: "head"
3117
+ });
3118
+ }
3493
3119
  }
3120
+ } else {
3121
+ // inject the reset style only in the default template,
3122
+ // allowing users to customize the style in their own template
3123
+ testerTags.push({
3124
+ tag: "style",
3125
+ children: `
3126
+ html {
3127
+ padding: 0;
3128
+ margin: 0;
3129
+ }
3130
+ body {
3131
+ padding: 0;
3132
+ margin: 0;
3133
+ min-height: 100vh;
3134
+ }`,
3135
+ injectTo: "head"
3136
+ });
3494
3137
  }
3138
+ return [
3139
+ {
3140
+ tag: "script",
3141
+ children: "{__VITEST_INJECTOR__}",
3142
+ injectTo: "head-prepend"
3143
+ },
3144
+ {
3145
+ tag: "script",
3146
+ children: stateJs,
3147
+ injectTo: "head-prepend"
3148
+ },
3149
+ {
3150
+ tag: "script",
3151
+ attrs: {
3152
+ type: "module",
3153
+ src: parentServer.errorCatcherUrl
3154
+ },
3155
+ injectTo: "head"
3156
+ },
3157
+ {
3158
+ tag: "script",
3159
+ attrs: {
3160
+ type: "module",
3161
+ src: parentServer.matchersUrl
3162
+ },
3163
+ injectTo: "head"
3164
+ },
3165
+ ...parentServer.initScripts.map((script) => ({
3166
+ tag: "script",
3167
+ attrs: {
3168
+ type: "module",
3169
+ src: join("/@fs/", script)
3170
+ },
3171
+ injectTo: "head"
3172
+ })),
3173
+ ...testerTags
3174
+ ].filter((s) => s != null);
3495
3175
  },
3496
- mode: project.config.mode,
3497
- configFile: configPath,
3498
- configLoader: project.vite.config.inlineConfig.configLoader,
3499
- server: {
3500
- ...project.options?.server,
3501
- hmr: false,
3502
- watch: null
3176
+ configureServer(server) {
3177
+ const parentServer = contribution.parent;
3178
+ parentServer.setServer(server);
3179
+ // eslint-disable-next-line prefer-arrow-callback
3180
+ server.middlewares.use(function vitestHeaders(_req, res, next) {
3181
+ const headers = server.config.server.headers;
3182
+ if (headers) {
3183
+ for (const name in headers) {
3184
+ res.setHeader(name, headers[name]);
3185
+ }
3186
+ }
3187
+ next();
3188
+ });
3189
+ // strip _vitest_original query added by importActual so that
3190
+ // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
3191
+ server.middlewares.use((req, _res, next) => {
3192
+ if (req.url?.includes("_vitest_original") && parentServer.config.browser.provider?.name === "playwright") {
3193
+ req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
3194
+ }
3195
+ next();
3196
+ });
3197
+ server.middlewares.use(createOrchestratorMiddleware(parentServer));
3198
+ server.middlewares.use(createTesterMiddleware(parentServer));
3199
+ server.middlewares.use(`/favicon.svg`, (_, res) => {
3200
+ const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
3201
+ res.write(content, "utf-8");
3202
+ res.end();
3203
+ });
3204
+ // Serve coverage HTML at ./coverage if configured
3205
+ const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
3206
+ if (coverageHtmlDir) {
3207
+ server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
3208
+ single: true,
3209
+ dev: true,
3210
+ setHeaders: (res) => {
3211
+ const csp = res.getHeader("Content-Security-Policy");
3212
+ if (typeof csp === "string") {
3213
+ // add frame-ancestors to allow the iframe to be loaded by Vitest,
3214
+ // but keep the rest of the CSP
3215
+ res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
3216
+ }
3217
+ res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
3218
+ }
3219
+ }));
3220
+ }
3221
+ server.middlewares.use((req, res, next) => {
3222
+ // 9000 mega head move
3223
+ // Vite always caches optimized dependencies, but users might mock
3224
+ // them in _some_ tests, while keeping original modules in others
3225
+ // there is no way to configure that in Vite, so we patch it here
3226
+ // to always ignore the cache-control set by Vite in the next middleware
3227
+ if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
3228
+ res.setHeader("Cache-Control", "no-cache");
3229
+ const setHeader = res.setHeader.bind(res);
3230
+ res.setHeader = function(name, value) {
3231
+ if (name === "Cache-Control") {
3232
+ return res;
3233
+ }
3234
+ return setHeader(name, value);
3235
+ };
3236
+ }
3237
+ next();
3238
+ });
3239
+ // handle attachments the same way as in packages/ui/node/index.ts
3240
+ server.middlewares.use((req, res, next) => {
3241
+ if (!req.url) {
3242
+ return next();
3243
+ }
3244
+ const url = new URL(req.url, "http://localhost");
3245
+ if (url.pathname !== "/__vitest_attachment__") {
3246
+ return next();
3247
+ }
3248
+ const path = url.searchParams.get("path");
3249
+ const contentType = url.searchParams.get("contentType");
3250
+ if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
3251
+ return next();
3252
+ }
3253
+ const fsPath = decodeURIComponent(path);
3254
+ if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
3255
+ return next();
3256
+ }
3257
+ try {
3258
+ res.setHeader("content-type", contentType);
3259
+ return createReadStream(fsPath).pipe(res).on("close", () => res.end());
3260
+ } catch (err) {
3261
+ return next(err);
3262
+ }
3263
+ });
3264
+ // When the Vitest UI (`test.ui`) is enabled, the `vitest:ui` plugin owns
3265
+ // `/__vitest__` (including the token-injected index.html and its assets),
3266
+ // so registering sirv here would shadow it and serve the page without a token.
3267
+ if (!parentServer.vitest.config.ui) {
3268
+ server.middlewares.use("/__vitest__", sirv(distClientRoot));
3269
+ }
3503
3270
  },
3504
- cacheDir: project.vite.config.cacheDir,
3505
- plugins: [
3506
- {
3507
- name: "vitest-internal:browser-cacheDir",
3508
- configResolved(config) {
3509
- cacheDir = config.cacheDir;
3271
+ async config(viteConfig) {
3272
+ const testConfig = viteConfig.test || {};
3273
+ const config = {
3274
+ resolve: {
3275
+ alias: testConfig.alias,
3276
+ dedupe: ["vitest"]
3277
+ },
3278
+ server: {
3279
+ fs: { allow: [distRoot] },
3280
+ middlewareMode: false,
3281
+ watch: null,
3282
+ forwardConsole: false
3510
3283
  }
3511
- },
3512
- ...options.mocksPlugins({ filter(id) {
3513
- if (id.includes(distRoot) || id.includes(cacheDir)) {
3514
- return false;
3284
+ };
3285
+ // Enables using the @preserve ignore hint for coverage providers. Only for
3286
+ // esbuild (rolldown-vite uses oxc instead).
3287
+ if (!rolldownVersion && viteConfig.esbuild !== false) {
3288
+ config.esbuild = { legalComments: "inline" };
3289
+ }
3290
+ const define = {};
3291
+ const envVars = testConfig.env || {};
3292
+ for (const env in envVars) {
3293
+ define[`import.meta.env.${env}`] = JSON.stringify(envVars[env]);
3294
+ }
3295
+ config.define = define;
3296
+ return config;
3297
+ },
3298
+ resolveOptimizeDeps(projectConfigs, testFiles, harness) {
3299
+ return resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness);
3300
+ },
3301
+ plugins: [],
3302
+ createParent({ config, vitest }) {
3303
+ if (vitest.version !== version) {
3304
+ vitest.logger.warn(c.yellow(`Loaded ${c.inverse(c.yellow(` vitest@${vitest.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + "\nRunning mixed versions is not supported and may lead into bugs" + "\nUpdate your dependencies and make sure the versions match."));
3305
+ }
3306
+ return new ParentBrowserProject({
3307
+ config,
3308
+ vitest
3309
+ }, "/");
3310
+ },
3311
+ setupRpc(parent) {
3312
+ setupBrowserRpc(parent, mockerRegistry);
3313
+ }
3314
+ };
3315
+ contribution.plugins = [
3316
+ ...BrowserPlugin(contribution),
3317
+ interceptorPlugin({ registry: mockerRegistry }),
3318
+ {
3319
+ name: "vitest:browser:framework-sourcemaps",
3320
+ enforce: "post",
3321
+ transform(code, id) {
3322
+ const parentServer = contribution.parent;
3323
+ // In a headless run nothing can open devtools, so sourcemaps of
3324
+ // Vitest's own pre-built modules are never consumed: their stack
3325
+ // frames are filtered by stackIgnorePatterns. Generating and
3326
+ // inlining these maps costs server CPU and multiplies the bytes
3327
+ // the browser downloads by ~5 for every fresh browser context.
3328
+ // Sourcemaps of user files and (by default) their dependencies are
3329
+ // kept — they point error stacks and devtools at original sources.
3330
+ if (!parentServer || !isHeadlessServer(parentServer) || parentServer.vitest.config.inspector.enabled) {
3331
+ return null;
3515
3332
  }
3516
- return true;
3517
- } }),
3518
- options.metaEnvReplacer(),
3519
- ...project.options?.plugins || [],
3520
- BrowserPlugin(server),
3521
- interceptorPlugin({ registry: mockerRegistry }),
3522
- options.coveragePlugin()
3523
- ]
3524
- });
3525
- await vite.listen();
3526
- setupBrowserRpc(server, mockerRegistry);
3527
- return server;
3333
+ if (isCSSRequest(id)) {
3334
+ return null;
3335
+ }
3336
+ const path = cleanUrl$1(id);
3337
+ if (path.startsWith(distRoot) || path.startsWith(distDir)) {
3338
+ return {
3339
+ code,
3340
+ map: { mappings: "" }
3341
+ };
3342
+ }
3343
+ // users that never debug into node_modules can drop dependency
3344
+ // sourcemaps entirely; `server.sourcemapIgnoreList` (default:
3345
+ // node_modules) can opt paths back in even then, e.g. with
3346
+ // `preserveSymlinks` where workspace code keeps its node_modules
3347
+ // path and would be wrongly treated as a dependency
3348
+ if (parentServer.config.browser.dependencySourcemaps === false && path.includes("/node_modules/") && (parentServer.vite.config.server.sourcemapIgnoreList(path, path) ?? true)) {
3349
+ return {
3350
+ code,
3351
+ map: { mappings: "" }
3352
+ };
3353
+ }
3354
+ return null;
3355
+ }
3356
+ }
3357
+ ];
3358
+ return contribution;
3528
3359
  };
3360
+ function isHeadlessServer(parentServer) {
3361
+ if (!parentServer.config.browser.headless) {
3362
+ return false;
3363
+ }
3364
+ // sibling instances share this server (and its module graph cache, so a
3365
+ // late-spawned instance would receive already-cached transforms) and can
3366
+ // override `headless` — check the static instance options instead of the
3367
+ // lazily populated `children` to stay deterministic across runs
3368
+ const instances = parentServer.config.browser.instances ?? [];
3369
+ return instances.every((instance) => instance.headless !== false);
3370
+ }
3371
+ function resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness) {
3372
+ // `testFiles` are globbed by the core package and aggregated across every
3373
+ // project that shares this browser Vite server (instance and benchmark
3374
+ // variants). The remaining options are shared by those projects, so the first
3375
+ // config is representative.
3376
+ const testConfig = projectConfigs[0];
3377
+ const root = testConfig.root || process.cwd();
3378
+ const setupFiles = new Set(projectConfigs.flatMap((config) => toArray(config.setupFiles || [])));
3379
+ const entries = [
3380
+ ...testFiles,
3381
+ ...setupFiles,
3382
+ resolve(distDir, "index.js"),
3383
+ resolve(distDir, "browser.js"),
3384
+ resolve(distDir, "runners.js"),
3385
+ resolve(distDir, "utils.js"),
3386
+ ...testConfig.snapshotSerializers || []
3387
+ ];
3388
+ // Keep these external (never pre-bundle by optimizer):
3389
+ // - vitest/browser, @vitest/browser/context, @vitest/browser/utils are
3390
+ // VIRTUAL modules generated per-server (see pluginContext.ts) — optimizer
3391
+ // cannot resolve/run their `load`, it would freeze stale/empty content.
3392
+ // - vite/module-runner is small enough to not need pre-bundling.
3393
+ // - msw is a large, side-effectful service-worker library.
3394
+ const exclude = [
3395
+ "vitest/browser",
3396
+ "vite/module-runner",
3397
+ "@vitest/browser/utils",
3398
+ "@vitest/browser/context",
3399
+ "msw",
3400
+ "msw/browser"
3401
+ ];
3402
+ if (typeof testConfig.diff === "string") {
3403
+ entries.push(testConfig.diff);
3404
+ }
3405
+ if (testConfig.coverage?.enabled) {
3406
+ const provider = testConfig.coverage.provider ?? "v8";
3407
+ if (provider === "v8") {
3408
+ const path = tryResolve("@vitest/coverage-v8", [root]);
3409
+ if (path) {
3410
+ entries.push(path);
3411
+ exclude.push("@vitest/coverage-v8/browser");
3412
+ }
3413
+ } else if (provider === "istanbul") {
3414
+ const path = tryResolve("@vitest/coverage-istanbul", [root]);
3415
+ if (path) {
3416
+ entries.push(path);
3417
+ exclude.push("@vitest/coverage-istanbul");
3418
+ }
3419
+ } else if (provider === "custom" && testConfig.coverage.customProviderModule) {
3420
+ entries.push(testConfig.coverage.customProviderModule);
3421
+ }
3422
+ }
3423
+ // Pre-bundle the vitest runtime so the browser fetches a few optimized
3424
+ // chunks instead of ~20 separately-served dist chunks (faster startup).
3425
+ // `vitest`, `vitest/internal/browser` and `@vitest/browser/client` are
3426
+ // optimized together in a single pass, so esbuild dedupes their shared
3427
+ // stateful chunks (the test collector, the runner, the RPC client) to a
3428
+ // single instance — preserving module identity between the test files'
3429
+ // `import 'vitest'` and the tester. Their transitive deps (@vitest/utils,
3430
+ // @vitest/spy, pathe, tinyrainbow, …) are inlined into these bundles.
3431
+ const include = [
3432
+ "vitest > expect-type",
3433
+ "vitest > magic-string",
3434
+ "vitest > chai",
3435
+ "vitest",
3436
+ "vitest/internal/browser",
3437
+ "@vitest/browser/client"
3438
+ ];
3439
+ const provider = testConfig.browser?.provider;
3440
+ if (provider?.name === "preview") {
3441
+ include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
3442
+ }
3443
+ const isPackageExists = (pkg) => {
3444
+ return harness.packageInstaller.isPackageExists(pkg, { paths: [root] });
3445
+ };
3446
+ if (isPackageExists("vitest-browser-svelte")) {
3447
+ exclude.push("vitest-browser-svelte");
3448
+ }
3449
+ // since we override the resolution in the esbuild plugin, Vite can no longer optimize it
3450
+ if (isPackageExists("vitest-browser-vue")) {
3451
+ include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
3452
+ }
3453
+ if (isPackageExists("@vue/test-utils")) {
3454
+ include.push("@vue/test-utils");
3455
+ }
3456
+ const otelConfig = testConfig.experimental?.openTelemetry;
3457
+ if (otelConfig?.enabled && otelConfig.browserSdkPath) {
3458
+ entries.push(otelConfig.browserSdkPath);
3459
+ include.push("@opentelemetry/api");
3460
+ } else {
3461
+ exclude.push("@opentelemetry/api");
3462
+ }
3463
+ // resolve @vue/(test-utils|compiler-core) to CJS instead of the browser build:
3464
+ // test-utils' browser build expects a global Vue; compiler-core's CJS build is
3465
+ // the only one that allows slots as strings.
3466
+ const rolldownPlugin = {
3467
+ name: "vue-test-utils-rewrite",
3468
+ resolveId: {
3469
+ filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
3470
+ handler(source, importer) {
3471
+ return getRequire().resolve(source, { paths: [importer] });
3472
+ }
3473
+ }
3474
+ };
3475
+ const esbuildPlugin = {
3476
+ name: "test-utils-rewrite",
3477
+ setup(build) {
3478
+ build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
3479
+ return { path: getRequire().resolve(args.path, { paths: [args.importer] }) };
3480
+ });
3481
+ }
3482
+ };
3483
+ return {
3484
+ entries,
3485
+ exclude,
3486
+ include,
3487
+ ...rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } }
3488
+ };
3489
+ }
3490
+ function tryResolve(path, paths) {
3491
+ try {
3492
+ return getRequire().resolve(path, { paths });
3493
+ } catch {
3494
+ return undefined;
3495
+ }
3496
+ }
3497
+ let _require;
3498
+ function getRequire() {
3499
+ if (!_require) {
3500
+ _require = createRequire(import.meta.url);
3501
+ }
3502
+ return _require;
3503
+ }
3529
3504
  function defineBrowserProvider(options) {
3530
3505
  return {
3531
3506
  ...options,