@vitest/browser 3.1.1 → 3.1.3

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 (30) hide show
  1. package/dist/client/.vite/manifest.json +6 -6
  2. package/dist/client/__vitest__/assets/index-Cv3XDLXs.js +52 -0
  3. package/dist/client/__vitest__/assets/{index-B0KEk_KY.css → index-D6BhetW8.css} +1 -1
  4. package/dist/client/__vitest__/index.html +2 -2
  5. package/dist/client/__vitest_browser__/orchestrator-CuTjqoE1.js +287 -0
  6. package/dist/client/__vitest_browser__/{tester-DiLSqOx4.js → tester-D8qCxA_3.js} +3172 -3084
  7. package/dist/client/__vitest_browser__/{utils-CNTxSNQV.js → utils-Owv5OOOf.js} +2 -2
  8. package/dist/client/error-catcher.js +1 -14
  9. package/dist/client/esm-client-injector.js +0 -1
  10. package/dist/client/orchestrator.html +2 -2
  11. package/dist/client/tester/tester.html +2 -2
  12. package/dist/client.js +63 -23
  13. package/dist/context.js +4 -4
  14. package/dist/expect-element.js +1 -1
  15. package/dist/index-C3ICQ6zz.js +1 -0
  16. package/dist/index.d.ts +8 -8
  17. package/dist/index.js +364 -244
  18. package/dist/locators/index.js +1 -1
  19. package/dist/locators/playwright.js +1 -1
  20. package/dist/locators/preview.js +1 -1
  21. package/dist/locators/webdriverio.js +1 -1
  22. package/dist/providers.js +2 -1
  23. package/dist/{public-utils-xf4CCUzp.js → public-utils-DUr23h1p.js} +2 -2
  24. package/dist/state.js +2 -67
  25. package/dist/utils.js +1 -1
  26. package/dist/{webdriver-2iYWIzBv.js → webdriver-BH7t2pDp.js} +63 -9
  27. package/package.json +16 -16
  28. package/dist/client/__vitest__/assets/index-BLZJq7cG.js +0 -52
  29. package/dist/client/__vitest_browser__/orchestrator-CqPXjvQE.js +0 -241
  30. package/dist/index-DjDyxzt8.js +0 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import c from 'tinyrainbow';
4
4
  import { getFilePoolName, distDir, resolveApiServerConfig, resolveFsAllow, isFileServingAllowed, createDebugger, isValidApiRequest, createViteLogger, createViteServer } from 'vitest/node';
5
5
  import fs, { readFileSync, lstatSync, promises, existsSync } from 'node:fs';
6
6
  import { createRequire } from 'node:module';
7
- import { slash as slash$1, toArray } from '@vitest/utils';
7
+ import { slash as slash$1, toArray, createDefer } from '@vitest/utils';
8
8
  import MagicString from 'magic-string';
9
9
  import sirv from 'sirv';
10
10
  import * as vite from 'vite';
@@ -13,12 +13,12 @@ import { fileURLToPath } from 'node:url';
13
13
  import crypto from 'node:crypto';
14
14
  import { mkdir, readFile as readFile$1 } from 'node:fs/promises';
15
15
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
16
- import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-2iYWIzBv.js';
16
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-BH7t2pDp.js';
17
17
  import { resolve as resolve$1, dirname as dirname$1, basename as basename$1, normalize as normalize$1 } from 'node:path';
18
18
  import { WebSocketServer } from 'ws';
19
19
  import * as nodeos from 'node:os';
20
20
 
21
- var version = "3.1.1";
21
+ var version = "3.1.3";
22
22
 
23
23
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
24
24
  function normalizeWindowsPath(input = "") {
@@ -213,6 +213,113 @@ const basename = function(p, extension) {
213
213
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
214
214
  const distRoot = resolve(pkgRoot, "dist");
215
215
 
216
+ /// <reference types="../types/index.d.ts" />
217
+
218
+ // (c) 2020-present Andrea Giammarchi
219
+
220
+ const {parse: $parse, stringify: $stringify} = JSON;
221
+ const {keys} = Object;
222
+
223
+ const Primitive = String; // it could be Number
224
+ const primitive = 'string'; // it could be 'number'
225
+
226
+ const ignore = {};
227
+ const object = 'object';
228
+
229
+ const noop = (_, value) => value;
230
+
231
+ const primitives = value => (
232
+ value instanceof Primitive ? Primitive(value) : value
233
+ );
234
+
235
+ const Primitives = (_, value) => (
236
+ typeof value === primitive ? new Primitive(value) : value
237
+ );
238
+
239
+ const revive = (input, parsed, output, $) => {
240
+ const lazy = [];
241
+ for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
242
+ const k = ke[y];
243
+ const value = output[k];
244
+ if (value instanceof Primitive) {
245
+ const tmp = input[value];
246
+ if (typeof tmp === object && !parsed.has(tmp)) {
247
+ parsed.add(tmp);
248
+ output[k] = ignore;
249
+ lazy.push({k, a: [input, parsed, tmp, $]});
250
+ }
251
+ else
252
+ output[k] = $.call(output, k, tmp);
253
+ }
254
+ else if (output[k] !== ignore)
255
+ output[k] = $.call(output, k, value);
256
+ }
257
+ for (let {length} = lazy, i = 0; i < length; i++) {
258
+ const {k, a} = lazy[i];
259
+ output[k] = $.call(output, k, revive.apply(null, a));
260
+ }
261
+ return output;
262
+ };
263
+
264
+ const set = (known, input, value) => {
265
+ const index = Primitive(input.push(value) - 1);
266
+ known.set(value, index);
267
+ return index;
268
+ };
269
+
270
+ /**
271
+ * Converts a specialized flatted string into a JS value.
272
+ * @param {string} text
273
+ * @param {(this: any, key: string, value: any) => any} [reviver]
274
+ * @returns {any}
275
+ */
276
+ const parse = (text, reviver) => {
277
+ const input = $parse(text, Primitives).map(primitives);
278
+ const value = input[0];
279
+ const $ = reviver || noop;
280
+ const tmp = typeof value === object && value ?
281
+ revive(input, new Set, value, $) :
282
+ value;
283
+ return $.call({'': tmp}, '', tmp);
284
+ };
285
+
286
+ /**
287
+ * Converts a JS value into a specialized flatted string.
288
+ * @param {any} value
289
+ * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
290
+ * @param {string | number | undefined} [space]
291
+ * @returns {string}
292
+ */
293
+ const stringify = (value, replacer, space) => {
294
+ const $ = replacer && typeof replacer === object ?
295
+ (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
296
+ (replacer || noop);
297
+ const known = new Map;
298
+ const input = [];
299
+ const output = [];
300
+ let i = +set(known, input, $.call({'': value}, '', value));
301
+ let firstRun = !i;
302
+ while (i < input.length) {
303
+ firstRun = true;
304
+ output[i] = $stringify(input[i++], replace, space);
305
+ }
306
+ return '[' + output.join(',') + ']';
307
+ function replace(key, value) {
308
+ if (firstRun) {
309
+ firstRun = !firstRun;
310
+ return value;
311
+ }
312
+ const after = $.call(this, key, value);
313
+ switch (typeof after) {
314
+ case object:
315
+ if (after === null) return after;
316
+ case primitive:
317
+ return known.get(after) || set(known, input, after);
318
+ }
319
+ return after;
320
+ }
321
+ };
322
+
216
323
  function replacer(code, values) {
217
324
  return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _);
218
325
  }
@@ -249,22 +356,23 @@ async function resolveOrchestrator(globalServer, url, res) {
249
356
  sessionId = contexts[contexts.length - 1] ?? "none";
250
357
  }
251
358
  const session = globalServer.vitest._browserSessions.getSession(sessionId);
252
- const files = session?.files ?? [];
253
359
  const browserProject = session?.project.browser || [...globalServer.children][0];
254
360
  if (!browserProject) {
255
361
  return;
256
362
  }
363
+ if (sessionId && sessionId !== "none" && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
364
+ return;
365
+ }
257
366
  const injectorJs = typeof globalServer.injectorJs === "string" ? globalServer.injectorJs : await globalServer.injectorJs;
258
367
  const injector = replacer(injectorJs, {
259
368
  __VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider || "preview"),
260
369
  __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
261
370
  __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
262
- __VITEST_METHOD__: JSON.stringify(session?.method || "run"),
263
- __VITEST_FILES__: JSON.stringify(files),
371
+ __VITEST_METHOD__: JSON.stringify("orchestrate"),
264
372
  __VITEST_TYPE__: "\"orchestrator\"",
265
373
  __VITEST_SESSION_ID__: JSON.stringify(sessionId),
266
374
  __VITEST_TESTER_ID__: "\"none\"",
267
- __VITEST_PROVIDED_CONTEXT__: "{}",
375
+ __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(browserProject.project.getProvidedContext())),
268
376
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
269
377
  });
270
378
  res.removeHeader("Content-Security-Policy");
@@ -327,119 +435,12 @@ function createOrchestratorMiddleware(parentServer) {
327
435
  };
328
436
  }
329
437
 
330
- /// <reference types="../types/index.d.ts" />
331
-
332
- // (c) 2020-present Andrea Giammarchi
333
-
334
- const {parse: $parse, stringify: $stringify} = JSON;
335
- const {keys} = Object;
336
-
337
- const Primitive = String; // it could be Number
338
- const primitive = 'string'; // it could be 'number'
339
-
340
- const ignore = {};
341
- const object = 'object';
342
-
343
- const noop = (_, value) => value;
344
-
345
- const primitives = value => (
346
- value instanceof Primitive ? Primitive(value) : value
347
- );
348
-
349
- const Primitives = (_, value) => (
350
- typeof value === primitive ? new Primitive(value) : value
351
- );
352
-
353
- const revive = (input, parsed, output, $) => {
354
- const lazy = [];
355
- for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
356
- const k = ke[y];
357
- const value = output[k];
358
- if (value instanceof Primitive) {
359
- const tmp = input[value];
360
- if (typeof tmp === object && !parsed.has(tmp)) {
361
- parsed.add(tmp);
362
- output[k] = ignore;
363
- lazy.push({k, a: [input, parsed, tmp, $]});
364
- }
365
- else
366
- output[k] = $.call(output, k, tmp);
367
- }
368
- else if (output[k] !== ignore)
369
- output[k] = $.call(output, k, value);
370
- }
371
- for (let {length} = lazy, i = 0; i < length; i++) {
372
- const {k, a} = lazy[i];
373
- output[k] = $.call(output, k, revive.apply(null, a));
374
- }
375
- return output;
376
- };
377
-
378
- const set = (known, input, value) => {
379
- const index = Primitive(input.push(value) - 1);
380
- known.set(value, index);
381
- return index;
382
- };
383
-
384
- /**
385
- * Converts a specialized flatted string into a JS value.
386
- * @param {string} text
387
- * @param {(this: any, key: string, value: any) => any} [reviver]
388
- * @returns {any}
389
- */
390
- const parse = (text, reviver) => {
391
- const input = $parse(text, Primitives).map(primitives);
392
- const value = input[0];
393
- const $ = reviver || noop;
394
- const tmp = typeof value === object && value ?
395
- revive(input, new Set, value, $) :
396
- value;
397
- return $.call({'': tmp}, '', tmp);
398
- };
399
-
400
- /**
401
- * Converts a JS value into a specialized flatted string.
402
- * @param {any} value
403
- * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
404
- * @param {string | number | undefined} [space]
405
- * @returns {string}
406
- */
407
- const stringify = (value, replacer, space) => {
408
- const $ = replacer && typeof replacer === object ?
409
- (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
410
- (replacer || noop);
411
- const known = new Map;
412
- const input = [];
413
- const output = [];
414
- let i = +set(known, input, $.call({'': value}, '', value));
415
- let firstRun = !i;
416
- while (i < input.length) {
417
- firstRun = true;
418
- output[i] = $stringify(input[i++], replace, space);
419
- }
420
- return '[' + output.join(',') + ']';
421
- function replace(key, value) {
422
- if (firstRun) {
423
- firstRun = !firstRun;
424
- return value;
425
- }
426
- const after = $.call(this, key, value);
427
- switch (typeof after) {
428
- case object:
429
- if (after === null) return after;
430
- case primitive:
431
- return known.get(after) || set(known, input, after);
432
- }
433
- return after;
434
- }
435
- };
436
-
437
438
  async function resolveTester(globalServer, url, res, next) {
438
439
  const csp = res.getHeader("Content-Security-Policy");
439
440
  if (typeof csp === "string") {
440
441
  res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
441
442
  }
442
- const { sessionId, testFile } = globalServer.resolveTesterUrl(url.pathname);
443
+ const sessionId = url.searchParams.get("sessionId") || "none";
443
444
  const session = globalServer.vitest._browserSessions.getSession(sessionId);
444
445
  if (!session) {
445
446
  res.statusCode = 400;
@@ -447,11 +448,6 @@ async function resolveTester(globalServer, url, res, next) {
447
448
  return;
448
449
  }
449
450
  const project = globalServer.vitest.getProjectByName(session.project.name || "");
450
- const { testFiles } = await project.globTestFiles();
451
- const tests = testFile === "__vitest_all__" || !testFiles.includes(testFile) ? "__vitest_browser_runner__.files" : JSON.stringify([testFile]);
452
- const iframeId = JSON.stringify(testFile);
453
- const files = session.files ?? [];
454
- const method = session.method ?? "run";
455
451
  const browserProject = project.browser || [...globalServer.children][0];
456
452
  if (!browserProject) {
457
453
  res.statusCode = 400;
@@ -462,13 +458,12 @@ async function resolveTester(globalServer, url, res, next) {
462
458
  const injector = replacer(injectorJs, {
463
459
  __VITEST_PROVIDER__: JSON.stringify(project.browser.provider.name),
464
460
  __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
465
- __VITEST_FILES__: JSON.stringify(files),
466
461
  __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
467
462
  __VITEST_TYPE__: "\"tester\"",
468
- __VITEST_METHOD__: JSON.stringify(method),
463
+ __VITEST_METHOD__: JSON.stringify("none"),
469
464
  __VITEST_SESSION_ID__: JSON.stringify(sessionId),
470
465
  __VITEST_TESTER_ID__: JSON.stringify(crypto.randomUUID()),
471
- __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())),
466
+ __VITEST_PROVIDED_CONTEXT__: "{}",
472
467
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
473
468
  });
474
469
  const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
@@ -477,17 +472,11 @@ async function resolveTester(globalServer, url, res, next) {
477
472
  const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml);
478
473
  const html = replacer(indexhtml, {
479
474
  __VITEST_FAVICON__: globalServer.faviconUrl,
480
- __VITEST_INJECTOR__: injector,
481
- __VITEST_APPEND__: `
482
- __vitest_browser_runner__.runningFiles = ${tests}
483
- __vitest_browser_runner__.iframeId = ${iframeId}
484
- __vitest_browser_runner__.${method === "run" ? "runTests" : "collectTests"}(__vitest_browser_runner__.runningFiles)
485
- document.querySelector('script[data-vitest-append]').remove()
486
- `
475
+ __VITEST_INJECTOR__: injector
487
476
  });
488
477
  return html;
489
478
  } catch (err) {
490
- session.reject(err);
479
+ session.fail(err);
491
480
  next(err);
492
481
  }
493
482
  }
@@ -498,7 +487,7 @@ function createTesterMiddleware(browserServer) {
498
487
  return next();
499
488
  }
500
489
  const url = new URL(req.url, "http://localhost");
501
- if (!url.pathname.startsWith(browserServer.prefixTesterUrl)) {
490
+ if (!url.pathname.startsWith(browserServer.prefixTesterUrl) || !url.searchParams.has("sessionId")) {
502
491
  return next();
503
492
  }
504
493
  const html = await resolveTester(browserServer, url, res, next);
@@ -961,16 +950,7 @@ body {
961
950
  injectTo: "head"
962
951
  } : null,
963
952
  ...parentServer.testerScripts,
964
- ...testerTags,
965
- {
966
- tag: "script",
967
- attrs: {
968
- "type": "module",
969
- "data-vitest-append": ""
970
- },
971
- children: "{__VITEST_APPEND__}",
972
- injectTo: "body"
973
- }
953
+ ...testerTags
974
954
  ].filter((s) => s != null);
975
955
  }
976
956
  },
@@ -1216,6 +1196,7 @@ const types = {
1216
1196
  'application/dash+xml': ['mpd'],
1217
1197
  'application/dash-patch+xml': ['mpp'],
1218
1198
  'application/davmount+xml': ['davmount'],
1199
+ 'application/dicom': ['dcm'],
1219
1200
  'application/docbook+xml': ['dbk'],
1220
1201
  'application/dssc+der': ['dssc'],
1221
1202
  'application/dssc+xml': ['xdssc'],
@@ -1302,7 +1283,14 @@ const types = {
1302
1283
  'application/oebps-package+xml': ['opf'],
1303
1284
  'application/ogg': ['ogx'],
1304
1285
  'application/omdoc+xml': ['omdoc'],
1305
- 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'],
1286
+ 'application/onenote': [
1287
+ 'onetoc',
1288
+ 'onetoc2',
1289
+ 'onetmp',
1290
+ 'onepkg',
1291
+ 'one',
1292
+ 'onea',
1293
+ ],
1306
1294
  'application/oxps': ['oxps'],
1307
1295
  'application/p2p-overlay+xml': ['relo'],
1308
1296
  'application/patch-ops-error+xml': ['xer'],
@@ -1398,6 +1386,7 @@ const types = {
1398
1386
  'application/yang': ['yang'],
1399
1387
  'application/yin+xml': ['yin'],
1400
1388
  'application/zip': ['zip'],
1389
+ 'application/zip+dotlottie': ['lottie'],
1401
1390
  'audio/3gpp': ['*3gpp'],
1402
1391
  'audio/aac': ['adts', 'aac'],
1403
1392
  'audio/adpcm': ['adp'],
@@ -1406,7 +1395,7 @@ const types = {
1406
1395
  'audio/midi': ['mid', 'midi', 'kar', 'rmi'],
1407
1396
  'audio/mobile-xmf': ['mxmf'],
1408
1397
  'audio/mp3': ['*mp3'],
1409
- 'audio/mp4': ['m4a', 'mp4a'],
1398
+ 'audio/mp4': ['m4a', 'mp4a', 'm4b'],
1410
1399
  'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'],
1411
1400
  'audio/ogg': ['oga', 'ogg', 'spx', 'opus'],
1412
1401
  'audio/s3m': ['s3m'],
@@ -1438,11 +1427,12 @@ const types = {
1438
1427
  'image/heif': ['heif'],
1439
1428
  'image/heif-sequence': ['heifs'],
1440
1429
  'image/hej2k': ['hej2'],
1441
- 'image/hsj2': ['hsj2'],
1442
1430
  'image/ief': ['ief'],
1431
+ 'image/jaii': ['jaii'],
1432
+ 'image/jais': ['jais'],
1443
1433
  'image/jls': ['jls'],
1444
1434
  'image/jp2': ['jp2', 'jpg2'],
1445
- 'image/jpeg': ['jpeg', 'jpg', 'jpe'],
1435
+ 'image/jpeg': ['jpg', 'jpeg', 'jpe'],
1446
1436
  'image/jph': ['jph'],
1447
1437
  'image/jphc': ['jhc'],
1448
1438
  'image/jpm': ['jpm', 'jpgm'],
@@ -1457,6 +1447,7 @@ const types = {
1457
1447
  'image/jxss': ['jxss'],
1458
1448
  'image/ktx': ['ktx'],
1459
1449
  'image/ktx2': ['ktx2'],
1450
+ 'image/pjpeg': ['jfif'],
1460
1451
  'image/png': ['png'],
1461
1452
  'image/sgi': ['sgi'],
1462
1453
  'image/svg+xml': ['svg', 'svgz'],
@@ -1470,7 +1461,7 @@ const types = {
1470
1461
  'message/global-delivery-status': ['u8dsn'],
1471
1462
  'message/global-disposition-notification': ['u8mdn'],
1472
1463
  'message/global-headers': ['u8hdr'],
1473
- 'message/rfc822': ['eml', 'mime'],
1464
+ 'message/rfc822': ['eml', 'mime', 'mht', 'mhtml'],
1474
1465
  'model/3mf': ['3mf'],
1475
1466
  'model/gltf+json': ['gltf'],
1476
1467
  'model/gltf-binary': ['glb'],
@@ -1480,6 +1471,7 @@ const types = {
1480
1471
  'model/mtl': ['mtl'],
1481
1472
  'model/obj': ['obj'],
1482
1473
  'model/prc': ['prc'],
1474
+ 'model/step': ['step', 'stp', 'stpnc', 'p21', '210'],
1483
1475
  'model/step+xml': ['stpx'],
1484
1476
  'model/step+zip': ['stpz'],
1485
1477
  'model/step-xml+zip': ['stpxz'],
@@ -1586,8 +1578,8 @@ class Mime {
1586
1578
  getType(path) {
1587
1579
  if (typeof path !== 'string')
1588
1580
  return null;
1589
- const last = path.replace(/^.*[/\\]/, '').toLowerCase();
1590
- const ext = last.replace(/^.*\./, '').toLowerCase();
1581
+ const last = path.replace(/^.*[/\\]/s, '').toLowerCase();
1582
+ const ext = last.replace(/^.*\./s, '').toLowerCase();
1591
1583
  const hasPath = last.length < path.length;
1592
1584
  const hasDot = ext.length < last.length - 1;
1593
1585
  if (!hasDot && hasPath)
@@ -2453,7 +2445,7 @@ const type = async (context, selector, text, options = {}) => {
2453
2445
  return { unreleased: Array.from(unreleased) };
2454
2446
  };
2455
2447
 
2456
- const upload = async (context, selector, files) => {
2448
+ const upload = async (context, selector, files, options) => {
2457
2449
  const testPath = context.testPath;
2458
2450
  if (!testPath) {
2459
2451
  throw new Error(`Cannot upload files outside of a test`);
@@ -2471,7 +2463,7 @@ const upload = async (context, selector, files) => {
2471
2463
  buffer: Buffer.from(file.base64, "base64")
2472
2464
  };
2473
2465
  });
2474
- await iframe.locator(selector).setInputFiles(playwrightFiles);
2466
+ await iframe.locator(selector).setInputFiles(playwrightFiles, options);
2475
2467
  } else if (context.provider instanceof WebdriverBrowserProvider) {
2476
2468
  for (const file of files) {
2477
2469
  if (typeof file !== "string") {
@@ -2634,7 +2626,7 @@ class ParentBrowserProject {
2634
2626
  if (mod) {
2635
2627
  return id;
2636
2628
  }
2637
- const resolvedPath = resolve(project.config.root, id.slice(1));
2629
+ const resolvedPath = resolve(this.vite.config.root, id.slice(1));
2638
2630
  const modUrl = this.vite.moduleGraph.getModuleById(resolvedPath);
2639
2631
  if (modUrl) {
2640
2632
  return resolvedPath;
@@ -2718,14 +2710,13 @@ class ParentBrowserProject {
2718
2710
  if (!provider.getCDPSession) {
2719
2711
  throw new Error(`CDP is not supported by the provider "${provider.name}".`);
2720
2712
  }
2721
- const promise = this.cdpSessionsPromises.get(rpcId) ?? await (async () => {
2713
+ const session = await this.cdpSessionsPromises.get(rpcId) ?? await (async () => {
2722
2714
  const promise = provider.getCDPSession(sessionId).finally(() => {
2723
2715
  this.cdpSessionsPromises.delete(rpcId);
2724
2716
  });
2725
2717
  this.cdpSessionsPromises.set(rpcId, promise);
2726
2718
  return promise;
2727
2719
  })();
2728
- const session = await promise;
2729
2720
  const rpc = browser.state.testers.get(rpcId);
2730
2721
  if (!rpc) {
2731
2722
  throw new Error(`Tester RPC "${rpcId}" was not established.`);
@@ -2781,6 +2772,8 @@ class ParentBrowserProject {
2781
2772
  }
2782
2773
  }
2783
2774
 
2775
+ const TYPE_REQUEST = "q";
2776
+ const TYPE_RESPONSE = "s";
2784
2777
  const DEFAULT_TIMEOUT = 6e4;
2785
2778
  function defaultSerialize(i) {
2786
2779
  return i;
@@ -2813,7 +2806,7 @@ function createBirpc(functions, options) {
2813
2806
  if (method === "then" && !eventNames.includes("then") && !("then" in functions))
2814
2807
  return void 0;
2815
2808
  const sendEvent = (...args) => {
2816
- post(serialize({ m: method, a: args, t: "q" }));
2809
+ post(serialize({ m: method, a: args, t: TYPE_REQUEST }));
2817
2810
  };
2818
2811
  if (eventNames.includes(method)) {
2819
2812
  sendEvent.asEvent = sendEvent;
@@ -2835,8 +2828,9 @@ function createBirpc(functions, options) {
2835
2828
  if (timeout >= 0) {
2836
2829
  timeoutId = setTimeout(() => {
2837
2830
  try {
2838
- options.onTimeoutError?.(method, args);
2839
- throw new Error(`[birpc] timeout on calling "${method}"`);
2831
+ const handleResult = options.onTimeoutError?.(method, args);
2832
+ if (handleResult !== true)
2833
+ throw new Error(`[birpc] timeout on calling "${method}"`);
2840
2834
  } catch (e) {
2841
2835
  reject(e);
2842
2836
  }
@@ -2853,17 +2847,24 @@ function createBirpc(functions, options) {
2853
2847
  return sendCall;
2854
2848
  }
2855
2849
  });
2856
- function close() {
2850
+ function close(error) {
2857
2851
  closed = true;
2858
2852
  rpcPromiseMap.forEach(({ reject, method }) => {
2859
- reject(new Error(`[birpc] rpc is closed, cannot call "${method}"`));
2853
+ reject(error || new Error(`[birpc] rpc is closed, cannot call "${method}"`));
2860
2854
  });
2861
2855
  rpcPromiseMap.clear();
2862
2856
  off(onMessage);
2863
2857
  }
2864
2858
  async function onMessage(data, ...extra) {
2865
- const msg = deserialize(data);
2866
- if (msg.t === "q") {
2859
+ let msg;
2860
+ try {
2861
+ msg = deserialize(data);
2862
+ } catch (e) {
2863
+ if (options.onGeneralError?.(e) !== true)
2864
+ throw e;
2865
+ return;
2866
+ }
2867
+ if (msg.t === TYPE_REQUEST) {
2867
2868
  const { m: method, a: args } = msg;
2868
2869
  let result, error;
2869
2870
  const fn = resolver ? resolver(method, functions[method]) : functions[method];
@@ -2879,7 +2880,26 @@ function createBirpc(functions, options) {
2879
2880
  if (msg.i) {
2880
2881
  if (error && options.onError)
2881
2882
  options.onError(error, method, args);
2882
- post(serialize({ t: "s", i: msg.i, r: result, e: error }), ...extra);
2883
+ if (error && options.onFunctionError) {
2884
+ if (options.onFunctionError(error, method, args) === true)
2885
+ return;
2886
+ }
2887
+ if (!error) {
2888
+ try {
2889
+ post(serialize({ t: TYPE_RESPONSE, i: msg.i, r: result }), ...extra);
2890
+ return;
2891
+ } catch (e) {
2892
+ error = e;
2893
+ if (options.onGeneralError?.(e, method, args) !== true)
2894
+ throw e;
2895
+ }
2896
+ }
2897
+ try {
2898
+ post(serialize({ t: TYPE_RESPONSE, i: msg.i, e: error }), ...extra);
2899
+ } catch (e) {
2900
+ if (options.onGeneralError?.(e, method, args) !== true)
2901
+ throw e;
2902
+ }
2883
2903
  }
2884
2904
  } else {
2885
2905
  const { i: ack, r: result, e: error } = msg;
@@ -2934,9 +2954,9 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2934
2954
  if (!sessionId || !rpcId || projectName == null) {
2935
2955
  return error(new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`));
2936
2956
  }
2937
- const method = searchParams.get("method");
2938
- if (method !== "run" && method !== "collect") {
2939
- return error(new Error(`[vitest] Method query in ${request.url} is invalid. Method should be either "run" or "collect".`));
2957
+ if (!vitest._browserSessions.sessionIds.has(sessionId)) {
2958
+ const ids = [...vitest._browserSessions.sessionIds].join(", ");
2959
+ return error(new Error(`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`));
2940
2960
  }
2941
2961
  if (type === "orchestrator") {
2942
2962
  const session = vitest._browserSessions.getSession(sessionId);
@@ -2948,7 +2968,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2948
2968
  }
2949
2969
  wss.handleUpgrade(request, socket, head, (ws) => {
2950
2970
  wss.emit("connection", ws, request);
2951
- const rpc = setupClient(project, rpcId, ws, method);
2971
+ const rpc = setupClient(project, rpcId, ws);
2952
2972
  const state = project.browser.state;
2953
2973
  const clients = type === "tester" ? state.testers : state.orchestrators;
2954
2974
  clients.set(rpcId, rpc);
@@ -2957,6 +2977,10 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2957
2977
  debug$1?.("[%s] Browser API disconnected from %s", rpcId, type);
2958
2978
  clients.delete(rpcId);
2959
2979
  globalServer.removeCDPHandler(rpcId);
2980
+ if (type === "orchestrator") {
2981
+ vitest._browserSessions.destroySession(sessionId);
2982
+ }
2983
+ rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
2960
2984
  });
2961
2985
  });
2962
2986
  });
@@ -2969,7 +2993,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2969
2993
  throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
2970
2994
  }
2971
2995
  }
2972
- function setupClient(project, rpcId, ws, method) {
2996
+ function setupClient(project, rpcId, ws) {
2973
2997
  const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config.server?.deps?.moduleDirectories });
2974
2998
  const mocker = project.browser?.provider.mocker;
2975
2999
  const rpc = createBirpc({
@@ -2980,21 +3004,21 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2980
3004
  }
2981
3005
  vitest.state.catchError(error, type);
2982
3006
  },
2983
- async onQueued(file) {
3007
+ async onQueued(method, file) {
2984
3008
  if (method === "collect") {
2985
3009
  vitest.state.collectFiles(project, [file]);
2986
3010
  } else {
2987
3011
  await vitest._testRun.enqueued(project, file);
2988
3012
  }
2989
3013
  },
2990
- async onCollected(files) {
3014
+ async onCollected(method, files) {
2991
3015
  if (method === "collect") {
2992
3016
  vitest.state.collectFiles(project, files);
2993
3017
  } else {
2994
3018
  await vitest._testRun.collected(project, files);
2995
3019
  }
2996
3020
  },
2997
- async onTaskUpdate(packs, events) {
3021
+ async onTaskUpdate(method, packs, events) {
2998
3022
  if (method === "collect") {
2999
3023
  vitest.state.updateTasks(packs);
3000
3024
  } else {
@@ -3004,7 +3028,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3004
3028
  onAfterSuiteRun(meta) {
3005
3029
  vitest.coverageProvider?.onAfterSuiteRun(meta);
3006
3030
  },
3007
- async sendLog(log) {
3031
+ async sendLog(method, log) {
3008
3032
  if (method === "collect") {
3009
3033
  vitest.state.updateUserLog(log);
3010
3034
  } else {
@@ -3088,10 +3112,6 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3088
3112
  }, provider.getCommandsContext(sessionId));
3089
3113
  return await commands[command](context, ...payload);
3090
3114
  },
3091
- finishBrowserTests(sessionId) {
3092
- debug$1?.("[%s] Finishing browser tests for session", sessionId);
3093
- return vitest._browserSessions.getSession(sessionId)?.resolve();
3094
- },
3095
3115
  resolveMock(rawId, importer, options) {
3096
3116
  return mockResolver.resolveMock(rawId, importer, options);
3097
3117
  },
@@ -3160,6 +3180,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3160
3180
  on: (fn) => ws.on("message", fn),
3161
3181
  eventNames: ["onCancel", "cdpEvent"],
3162
3182
  serialize: (data) => stringify(data, stringifyReplace),
3183
+ timeout: -1,
3163
3184
  deserialize: parse,
3164
3185
  onTimeoutError(functionName) {
3165
3186
  throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
@@ -3194,69 +3215,30 @@ function stringifyReplace(key, value) {
3194
3215
  }
3195
3216
 
3196
3217
  const debug = createDebugger("vitest:browser:pool");
3197
- async function waitForTests(method, sessionId, project, files) {
3198
- const context = project.vitest._browserSessions.createAsyncSession(method, sessionId, files, project);
3199
- return await context;
3200
- }
3201
3218
  function createBrowserPool(vitest) {
3202
3219
  const providers = new Set();
3203
- const executeTests = async (method, project, files) => {
3204
- vitest.state.clearFiles(project, files);
3205
- const browser = project.browser;
3206
- const threadsCount = getThreadsCount(project);
3207
- const provider = browser.provider;
3208
- providers.add(provider);
3209
- const resolvedUrls = browser.vite.resolvedUrls;
3220
+ const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
3221
+ const threadsCount = vitest.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
3222
+ const projectPools = new WeakMap();
3223
+ const ensurePool = (project) => {
3224
+ if (projectPools.has(project)) {
3225
+ return projectPools.get(project);
3226
+ }
3227
+ debug?.("creating pool for project %s", project.name);
3228
+ const resolvedUrls = project.browser.vite.resolvedUrls;
3210
3229
  const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
3211
3230
  if (!origin) {
3212
- throw new Error(`Can't find browser origin URL for project "${project.name}" when running tests for files "${files.join("\", \"")}"`);
3231
+ throw new Error(`Can't find browser origin URL for project "${project.name}"`);
3213
3232
  }
3214
- async function setBreakpoint(sessionId, file) {
3215
- if (!project.config.inspector.waitForDebugger) {
3216
- return;
3217
- }
3218
- if (!provider.getCDPSession) {
3219
- throw new Error("Unable to set breakpoint, CDP not supported");
3220
- }
3221
- const session = await provider.getCDPSession(sessionId);
3222
- await session.send("Debugger.enable", {});
3223
- await session.send("Debugger.setBreakpointByUrl", {
3224
- lineNumber: 0,
3225
- urlRegex: escapePathToRegexp(file)
3226
- });
3227
- }
3228
- const filesPerThread = Math.ceil(files.length / threadsCount);
3229
- const chunks = [];
3230
- for (let i = 0; i < files.length; i += filesPerThread) {
3231
- const chunk = files.slice(i, i + filesPerThread);
3232
- chunks.push(chunk);
3233
- }
3234
- debug?.(`[%s] Running %s tests in %s chunks (%s threads)`, project.name || "core", files.length, chunks.length, threadsCount);
3235
- const orchestrators = [...browser.state.orchestrators.entries()];
3236
- const promises = [];
3237
- chunks.forEach((files, index) => {
3238
- if (orchestrators[index]) {
3239
- const [sessionId, orchestrator] = orchestrators[index];
3240
- debug?.("Reusing orchestrator (session %s) for files: %s", sessionId, [...files.map((f) => relative(project.config.root, f))].join(", "));
3241
- const promise = waitForTests(method, sessionId, project, files);
3242
- const tester = orchestrator.createTesters(files).catch((error) => {
3243
- if (error instanceof Error && error.message.startsWith("[birpc] rpc is closed")) {
3244
- return;
3245
- }
3246
- return Promise.reject(error);
3247
- });
3248
- promises.push(promise, tester);
3249
- } else {
3250
- const sessionId = crypto.randomUUID();
3251
- const waitPromise = waitForTests(method, sessionId, project, files);
3252
- debug?.("Opening a new session %s for files: %s", sessionId, [...files.map((f) => relative(project.config.root, f))].join(", "));
3253
- const url = new URL("/", origin);
3254
- url.searchParams.set("sessionId", sessionId);
3255
- const page = provider.openPage(sessionId, url.toString(), () => setBreakpoint(sessionId, files[0]));
3256
- promises.push(page, waitPromise);
3257
- }
3233
+ const pool = new BrowserPool(project, {
3234
+ maxWorkers: getThreadsCount(project),
3235
+ origin
3258
3236
  });
3259
- await Promise.all(promises);
3237
+ projectPools.set(project, pool);
3238
+ vitest.onCancel(() => {
3239
+ pool.cancel();
3240
+ });
3241
+ return pool;
3260
3242
  };
3261
3243
  const runWorkspaceTests = async (method, specs) => {
3262
3244
  const groupedFiles = new Map();
@@ -3269,41 +3251,43 @@ function createBrowserPool(vitest) {
3269
3251
  vitest.onCancel(() => {
3270
3252
  isCancelled = true;
3271
3253
  });
3272
- for (const [project, files] of groupedFiles.entries()) {
3273
- if (isCancelled) {
3274
- break;
3275
- }
3254
+ await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3276
3255
  await project._initBrowserProvider();
3277
3256
  if (!project.browser) {
3278
3257
  throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ""}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
3279
3258
  }
3280
- await executeTests(method, project, files);
3281
- }
3259
+ if (isCancelled) {
3260
+ return;
3261
+ }
3262
+ debug?.("provider is ready for %s project", project.name);
3263
+ const pool = ensurePool(project);
3264
+ vitest.state.clearFiles(project, files);
3265
+ providers.add(project.browser.provider);
3266
+ await pool.runTests(method, files);
3267
+ }));
3282
3268
  };
3283
- const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
3284
3269
  function getThreadsCount(project) {
3285
3270
  const config = project.config.browser;
3286
- if (!config.headless || !project.browser.provider.supportsParallelism) {
3287
- return 1;
3288
- }
3289
- if (!config.fileParallelism) {
3271
+ if (!config.headless || !config.fileParallelism || !project.browser.provider.supportsParallelism) {
3290
3272
  return 1;
3291
3273
  }
3292
3274
  if (project.config.maxWorkers) {
3293
3275
  return project.config.maxWorkers;
3294
3276
  }
3295
- return vitest.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
3277
+ return threadsCount;
3296
3278
  }
3297
3279
  return {
3298
3280
  name: "browser",
3299
3281
  async close() {
3300
3282
  await Promise.all([...providers].map((provider) => provider.close()));
3283
+ vitest._browserSessions.sessionIds.clear();
3301
3284
  providers.clear();
3302
3285
  vitest.projects.forEach((project) => {
3303
3286
  project.browser?.state.orchestrators.forEach((orchestrator) => {
3304
3287
  orchestrator.$close();
3305
3288
  });
3306
3289
  });
3290
+ debug?.("browser pool closed all providers");
3307
3291
  },
3308
3292
  runTests: (files) => runWorkspaceTests("run", files),
3309
3293
  collectTests: (files) => runWorkspaceTests("collect", files)
@@ -3312,6 +3296,141 @@ function createBrowserPool(vitest) {
3312
3296
  function escapePathToRegexp(path) {
3313
3297
  return path.replace(/[/\\.?*()^${}|[\]+]/g, "\\$&");
3314
3298
  }
3299
+ class BrowserPool {
3300
+ _queue = [];
3301
+ _promise;
3302
+ _providedContext;
3303
+ readySessions = new Set();
3304
+ constructor(project, options) {
3305
+ this.project = project;
3306
+ this.options = options;
3307
+ }
3308
+ cancel() {
3309
+ this._queue = [];
3310
+ }
3311
+ reject(error) {
3312
+ this._promise?.reject(error);
3313
+ this._promise = undefined;
3314
+ this.cancel();
3315
+ }
3316
+ get orchestrators() {
3317
+ return this.project.browser.state.orchestrators;
3318
+ }
3319
+ async runTests(method, files) {
3320
+ this._promise ??= createDefer();
3321
+ if (!files.length) {
3322
+ debug?.("no tests found, finishing test run immediately");
3323
+ this._promise.resolve();
3324
+ return this._promise;
3325
+ }
3326
+ this._providedContext = stringify(this.project.getProvidedContext());
3327
+ this._queue.push(...files);
3328
+ this.readySessions.forEach((sessionId) => {
3329
+ if (this._queue.length) {
3330
+ this.readySessions.delete(sessionId);
3331
+ this.runNextTest(method, sessionId);
3332
+ }
3333
+ });
3334
+ if (this.orchestrators.size >= this.options.maxWorkers) {
3335
+ debug?.("all orchestrators are ready, not creating more");
3336
+ return this._promise;
3337
+ }
3338
+ const workerCount = Math.min(this.options.maxWorkers - this.orchestrators.size, files.length);
3339
+ const promises = [];
3340
+ for (let i = 0; i < workerCount; i++) {
3341
+ const sessionId = crypto.randomUUID();
3342
+ this.project.vitest._browserSessions.sessionIds.add(sessionId);
3343
+ const project = this.project.name;
3344
+ debug?.("[%s] creating session for %s", sessionId, project);
3345
+ const page = this.openPage(sessionId).then(() => {
3346
+ this.runNextTest(method, sessionId);
3347
+ });
3348
+ promises.push(page);
3349
+ }
3350
+ await Promise.all(promises);
3351
+ debug?.("all sessions are created");
3352
+ return this._promise;
3353
+ }
3354
+ async openPage(sessionId) {
3355
+ const sessionPromise = this.project.vitest._browserSessions.createSession(sessionId, this.project, this);
3356
+ const url = new URL("/", this.options.origin);
3357
+ url.searchParams.set("sessionId", sessionId);
3358
+ const pagePromise = this.project.browser.provider.openPage(sessionId, url.toString());
3359
+ await Promise.all([sessionPromise, pagePromise]);
3360
+ }
3361
+ getOrchestrator(sessionId) {
3362
+ const orchestrator = this.orchestrators.get(sessionId);
3363
+ if (!orchestrator) {
3364
+ throw new Error(`Orchestrator not found for session ${sessionId}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
3365
+ }
3366
+ return orchestrator;
3367
+ }
3368
+ finishSession(sessionId) {
3369
+ this.readySessions.add(sessionId);
3370
+ if (this.readySessions.size === this.orchestrators.size) {
3371
+ this._promise?.resolve();
3372
+ this._promise = undefined;
3373
+ debug?.("[%s] all tests finished running", sessionId);
3374
+ } else {
3375
+ debug?.(`did not finish sessions for ${sessionId}: |ready - %s| |overall - %s|`, [...this.readySessions].join(", "), [...this.orchestrators.keys()].join(", "));
3376
+ }
3377
+ }
3378
+ runNextTest(method, sessionId) {
3379
+ const file = this._queue.shift();
3380
+ if (!file) {
3381
+ debug?.("[%s] no more tests to run", sessionId);
3382
+ const isolate = this.project.config.browser.isolate;
3383
+ if (isolate) {
3384
+ this.finishSession(sessionId);
3385
+ return;
3386
+ }
3387
+ const orchestrator = this.getOrchestrator(sessionId);
3388
+ orchestrator.cleanupTesters().catch((error) => this.reject(error)).finally(() => this.finishSession(sessionId));
3389
+ return;
3390
+ }
3391
+ if (!this._promise) {
3392
+ throw new Error(`Unexpected empty queue`);
3393
+ }
3394
+ const orchestrator = this.getOrchestrator(sessionId);
3395
+ debug?.("[%s] run test %s", sessionId, file);
3396
+ this.setBreakpoint(sessionId, file).then(() => {
3397
+ orchestrator.createTesters({
3398
+ method,
3399
+ files: [file],
3400
+ providedContext: this._providedContext || "[{}]"
3401
+ }).then(() => {
3402
+ debug?.("[%s] test %s finished running", sessionId, file);
3403
+ this.runNextTest(method, sessionId);
3404
+ }).catch((error) => {
3405
+ if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
3406
+ this.cancel();
3407
+ this._promise?.resolve();
3408
+ this._promise = undefined;
3409
+ debug?.("[%s] browser connection was closed", sessionId);
3410
+ return;
3411
+ }
3412
+ debug?.("[%s] error during %s test run: %s", sessionId, file, error);
3413
+ this.reject(error);
3414
+ });
3415
+ }).catch((err) => this.reject(err));
3416
+ }
3417
+ async setBreakpoint(sessionId, file) {
3418
+ if (!this.project.config.inspector.waitForDebugger) {
3419
+ return;
3420
+ }
3421
+ const provider = this.project.browser.provider;
3422
+ if (!provider.getCDPSession) {
3423
+ throw new Error("Unable to set breakpoint, CDP not supported");
3424
+ }
3425
+ debug?.("[%s] set breakpoint for %s", sessionId, file);
3426
+ const session = await provider.getCDPSession(sessionId);
3427
+ await session.send("Debugger.enable", {});
3428
+ await session.send("Debugger.setBreakpointByUrl", {
3429
+ lineNumber: 0,
3430
+ urlRegex: escapePathToRegexp(file)
3431
+ });
3432
+ }
3433
+ }
3315
3434
 
3316
3435
  async function createBrowserServer(project, configFile, prePlugins = [], postPlugins = []) {
3317
3436
  if (project.vitest.version !== version) {
@@ -3325,6 +3444,7 @@ async function createBrowserServer(project, configFile, prePlugins = [], postPlu
3325
3444
  const vite = await createViteServer({
3326
3445
  ...project.options,
3327
3446
  base: "/",
3447
+ root: project.config.root,
3328
3448
  logLevel,
3329
3449
  customLogger: {
3330
3450
  ...logger,