diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc4811c66980..57e8b3852476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,9 @@ jobs: - name: Test Single Thread run: pnpm run test:ci:single-thread + - name: Test Vm Threads + run: pnpm run test:ci:vm-threads + test-ui: runs-on: ubuntu-latest diff --git a/docs/config/index.md b/docs/config/index.md index 5722359d1b29..df930e232dc9 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -144,7 +144,11 @@ Handling for dependencies resolution. - **Type:** `(string | RegExp)[]` - **Default:** `[/\/node_modules\//]` -Externalize means that Vite will bypass the package to native Node. Externalized dependencies will not be applied Vite's transformers and resolvers, so they do not support HMR on reload. All packages under `node_modules` are externalized. +Externalize means that Vite will bypass the package to the native Node. Externalized dependencies will not be applied to Vite's transformers and resolvers, so they do not support HMR on reload. By default, all packages inside `node_modules` are externalized. + +These options support package names as they are written in `node_modules` or specified inside [`deps.moduleDirectories`](#deps-moduledirectories). For example, package `@company/some-name` located inside `packages/some-name` should be specified as `some-name`, and `packages` should be included in `deps.moduleDirectories`. Basically, Vitest always checks the file path, not the actual package name. + +If regexp is used, Vitest calls it on the _file path_, not the package name. #### server.deps.inline @@ -421,6 +425,7 @@ import type { Environment } from 'vitest' export default { name: 'custom', + transformMode: 'ssr', setup() { // custom setup return { @@ -468,7 +473,7 @@ export default defineConfig({ ### poolMatchGlobs -- **Type:** `[string, 'browser' | 'threads' | 'child_process'][]` +- **Type:** `[string, 'threads' | 'child_process' | 'experimentalVmThreads'][]` - **Default:** `[]` - **Version:** Since Vitest 0.29.4 @@ -542,6 +547,69 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith Write test results to a file when the `--reporter=json`, `--reporter=html` or `--reporter=junit` option is also specified. By providing an object instead of a string you can define individual outputs when using multiple reporters. +### experimentalVmThreads + +- **Type:** `boolean` +- **CLI:** `--experimentalVmThreads`, `--experimental-vm-threads` +- **Version:** Since Vitest 0.34.0 + +Run tests using [VM context](https://nodejs.org/api/vm.html) (inside a sandboxed environment) in a worker pool. + +This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`experimentalVmWorkerMemoryLimit`](#experimentalvmworkermemorylimit) value. + +::: warning +Running code in a sandbox has some advantages (faster tests), but also comes with a number of disadvantages. + +- The globals within native modules, such as (`fs`, `path`, etc), differ from the globals present in your test environment. As a result, any error thrown by these native modules will reference a different Error constructor compared to the one used in your code: + +```ts +try { + fs.writeFileSync('/doesnt exist') +} +catch (err) { + console.log(err instanceof Error) // false +} +``` + +- Importing ES modules caches them indefinitely which introduces memory leaks if you have a lot of contexts (test files). There is no API in Node.js that clears that cache. +- Accessing globals [takes longer](https://github.com/nodejs/node/issues/31658) in a sandbox environment. + +Please, be aware of these issues when using this option. Vitest team cannot fix any of the issues on our side. +::: + +### experimentalVmWorkerMemoryLimit + +- **Type:** `string | number` +- **CLI:** `--experimentalVmWorkerMemoryLimit`, `--experimental-vm-worker-memory-limit` +- **Default:** `1 / CPU Cores` +- **Version:** Since Vitest 0.34.0 + +Specifies the memory limit for workers before they are recycled. This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default. + +This option only affects workers that run tests in [VM context](#experimentalvmthreads). + +::: tip +The implementation is based on Jest's [`workerIdleMemoryLimit`](https://jestjs.io/docs/configuration#workeridlememorylimit-numberstring). + +The limit can be specified in a number of different ways and whatever the result is `Math.floor` is used to turn it into an integer value: + +- `<= 1` - The value is assumed to be a percentage of system memory. So 0.5 sets the memory limit of the worker to half of the total system memory +- `\> 1` - Assumed to be a fixed byte value. Because of the previous rule if you wanted a value of 1 byte (I don't know why) you could use 1.1. +- With units + - `50%` - As above, a percentage of total system memory + - `100KB`, `65MB`, etc - With units to denote a fixed memory limit. + - `K` / `KB` - Kilobytes (x1000) + - `KiB` - Kibibytes (x1024) + - `M` / `MB` - Megabytes + - `MiB` - Mebibytes + - `G` / `GB` - Gigabytes + - `GiB` - Gibibytes +::: + +::: warning +Percentage based memory limit [does not work on Linux CircleCI](https://github.com/jestjs/jest/issues/11956#issuecomment-1212925677) workers due to incorrect system memory being reported. +::: + ### threads - **Type:** `boolean` @@ -708,6 +776,8 @@ Make sure that your files are not excluded by `watchExclude`. Isolate environment for each test file. Does not work if you disable [`--threads`](#threads). +This options has no effect on [`experimentalVmThreads`](#experimentalvmthreads). + ### coverage You can use [`v8`](https://v8.dev/blog/javascript-code-coverage), [`istanbul`](https://istanbul.js.org/) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 11a3ad0ea9ec..818f793d5e9e 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -69,7 +69,10 @@ Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experim | `--ui` | Enable UI | | `--open` | Open the UI automatically if enabled (default: `true`) | | `--api [api]` | Serve API, available options: `--api.port `, `--api.host [host]` and `--api.strictPort` | -| `--threads` | Enable Threads (default: `true`) | +| `--threads` | Enable Threads (default: `true`) | +| `--single-thread` | Run tests inside a single thread, requires --threads (default: `false`) | +| `--experimental-vm-threads` | Run tests in a worker pool using VM isolation (default: `false`) | +| `--experimental-vm-worker-memory-limit` | Set the maximum allowed memory for a worker. When reached, a new worker will be created instead | | `--silent` | Silent console output from tests | | `--isolate` | Isolate environment for each test file (default: `true`) | | `--reporter ` | Select reporter: `default`, `verbose`, `dot`, `junit`, `json`, or a path to a custom reporter | diff --git a/docs/guide/environment.md b/docs/guide/environment.md index dfd365fa0316..b5494c109108 100644 --- a/docs/guide/environment.md +++ b/docs/guide/environment.md @@ -27,13 +27,27 @@ Or you can also set [`environmentMatchGlobs`](https://vitest.dev/config/#environ ## Custom Environment -Starting from 0.23.0, you can create your own package to extend Vitest environment. To do so, create package with the name `vitest-environment-${name}`. That package should export an object with the shape of `Environment`: +Starting from 0.23.0, you can create your own package to extend Vitest environment. To do so, create package with the name `vitest-environment-${name}` or specify a path to a valid JS file (supported since 0.34.0). That package should export an object with the shape of `Environment`: ```ts import type { Environment } from 'vitest' export default { name: 'custom', + transformMode: 'ssr', + // optional - only if you support "experimental-vm" pool + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + return { + getVmContext() { + return context + }, + teardown() { + // called after all tests with this env have been run + } + } + }, setup() { // custom setup return { @@ -45,6 +59,10 @@ export default { } ``` +::: warning +Since 0.34.0 Vitest requires `transformMode` option on environment object. It should be equal to `ssr` or `web`. This value determines how plugins will transform source code. If it's set to `ssr`, plugin hooks will receive `ssr: true` when transforming or resolving files. Otherwise, `ssr` is set to `false`. +::: + You also have access to default Vitest environments through `vitest/environments` entry: ```ts diff --git a/examples/mocks/vite.config.ts b/examples/mocks/vite.config.ts index 76cb1dad1e3e..ee914b24aaac 100644 --- a/examples/mocks/vite.config.ts +++ b/examples/mocks/vite.config.ts @@ -29,8 +29,12 @@ export default defineConfig({ test: { globals: true, environment: 'node', + server: { + deps: { + external: [/src\/external/], + }, + }, deps: { - external: [/src\/external/], interopDefault: true, moduleDirectories: ['node_modules', 'projects'], }, diff --git a/examples/solid/test/__snapshots__/Hello.test.jsx.snap b/examples/solid/test/__snapshots__/Hello.test.jsx.snap index 018959cb76da..2f7d062f3667 100644 --- a/examples/solid/test/__snapshots__/Hello.test.jsx.snap +++ b/examples/solid/test/__snapshots__/Hello.test.jsx.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1 +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[` > renders 1`] = `"
4 x 2 = 8
"`; diff --git a/examples/svelte/package.json b/examples/svelte/package.json index a1b2bde25082..3fdbc671f6dd 100644 --- a/examples/svelte/package.json +++ b/examples/svelte/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "latest", - "@testing-library/svelte": "latest", + "@testing-library/svelte": "^4.0.3", "@vitest/ui": "latest", "jsdom": "latest", "svelte": "latest", diff --git a/examples/vitesse/src/auto-import.d.ts b/examples/vitesse/src/auto-import.d.ts index 4259e89c6d15..9959df651b1b 100644 --- a/examples/vitesse/src/auto-import.d.ts +++ b/examples/vitesse/src/auto-import.d.ts @@ -1,6 +1,7 @@ /* eslint-disable */ /* prettier-ignore */ // @ts-nocheck +// noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import export {} declare global { diff --git a/package.json b/package.json index 3b9ee959ae95..f967a870ba02 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:run": "vitest run -r test/core", "test:all": "CI=true pnpm -r --stream run test --allowOnly", "test:ci": "CI=true pnpm -r --stream --filter !test-fails --filter !test-browser --filter !test-esm --filter !test-browser run test --allowOnly", + "test:ci:vm-threads": "CI=true pnpm -r --stream --filter !test-fails --filter !test-single-thread --filter !test-browser --filter !test-esm --filter !test-browser run test --allowOnly --experimental-vm-threads", "test:ci:single-thread": "CI=true pnpm -r --stream --filter !test-fails --filter !test-coverage --filter !test-watch --filter !test-bail --filter !test-esm --filter !test-browser run test --allowOnly --no-threads", "typecheck": "tsc --noEmit", "typecheck:why": "tsc --noEmit --explainFiles > explainTypes.txt", diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 046dbe7f941d..47171f97c036 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -68,6 +68,9 @@ ws.addEventListener('open', async () => { globalThis.__vitest_worker__ = { config, browserHashMap, + environment: { + name: 'browser', + }, // @ts-expect-error untyped global for internal use moduleCache: globalThis.__vi_module_cache__, rpc: client.rpc, diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index eb1ee9edb24e..25142152f9aa 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -29,7 +29,7 @@ export interface VitestRunnerConstructor { new(config: VitestRunnerConfig): VitestRunner } -export type CancelReason = 'keyboard-input' | 'test-failure' | string & {} +export type CancelReason = 'keyboard-input' | 'test-failure' | string & Record export interface VitestRunner { /** diff --git a/packages/vite-node/package.json b/packages/vite-node/package.json index f0f31f22ef74..641969e2864b 100644 --- a/packages/vite-node/package.json +++ b/packages/vite-node/package.json @@ -46,6 +46,11 @@ "require": "./dist/source-map.cjs", "import": "./dist/source-map.mjs" }, + "./constants": { + "types": "./dist/constants.d.ts", + "require": "./dist/constants.cjs", + "import": "./dist/constants.mjs" + }, "./*": "./*" }, "main": "./dist/index.mjs", diff --git a/packages/vite-node/rollup.config.js b/packages/vite-node/rollup.config.js index 7397ba5b4a25..258770c42e10 100644 --- a/packages/vite-node/rollup.config.js +++ b/packages/vite-node/rollup.config.js @@ -15,6 +15,7 @@ const entries = { 'client': 'src/client.ts', 'utils': 'src/utils.ts', 'cli': 'src/cli.ts', + 'constants': 'src/constants.ts', 'hmr': 'src/hmr/index.ts', 'source-map': 'src/source-map.ts', } @@ -29,6 +30,7 @@ const external = [ 'vite/types/hot', 'node:url', 'node:events', + 'node:vm', ] const plugins = [ diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index d2814e122f00..2c97f24f648b 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -17,7 +17,7 @@ const debugNative = createDebug('vite-node:client:native') const clientStub = { injectQuery: (id: string) => id, - createHotContext() { + createHotContext: () => { return { accept: () => {}, prune: () => {}, @@ -28,33 +28,11 @@ const clientStub = { send: () => {}, } }, - updateStyle(id: string, css: string) { - if (typeof document === 'undefined') - return - - const element = document.querySelector(`[data-vite-dev-id="${id}"]`) - if (element) { - element.textContent = css - return - } - - const head = document.querySelector('head') - const style = document.createElement('style') - style.setAttribute('type', 'text/css') - style.setAttribute('data-vite-dev-id', id) - style.textContent = css - head?.appendChild(style) - }, - removeStyle(id: string) { - if (typeof document === 'undefined') - return - const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`) - if (sheet) - document.head.removeChild(sheet) - }, + updateStyle: () => {}, + removeStyle: () => {}, } -export const DEFAULT_REQUEST_STUBS: Record = { +export const DEFAULT_REQUEST_STUBS: Record> = { '/@vite/client': clientStub, '@vite/client': clientStub, } @@ -304,7 +282,6 @@ export class ViteNodeRunner { const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS if (id in requestStubs) return requestStubs[id] - let { code: transformed, externalize } = await this.options.fetchModule(id) if (externalize) { @@ -317,6 +294,8 @@ export class ViteNodeRunner { if (transformed == null) throw new Error(`[vite-node] Failed to load "${id}" imported from ${callstack[callstack.length - 2]}`) + const { Object, Reflect, Symbol } = this.getContextPrimitives() + const modulePath = cleanUrl(moduleId) // disambiguate the `:/` on windows: see nodejs/node#31710 const href = pathToFileURL(modulePath).href @@ -416,18 +395,27 @@ export class ViteNodeRunner { if (transformed[0] === '#') transformed = transformed.replace(/^\#\!.*/, s => ' '.repeat(s.length)) + await this.runModule(context, transformed) + + return exports + } + + protected getContextPrimitives() { + return { Object, Reflect, Symbol } + } + + protected async runModule(context: Record, transformed: string) { // add 'use strict' since ESM enables it by default const codeDefinition = `'use strict';async (${Object.keys(context).join(',')})=>{{` const code = `${codeDefinition}${transformed}\n}}` - const fn = vm.runInThisContext(code, { - filename: __filename, + const options = { + filename: context.__filename, lineOffset: 0, columnOffset: -codeDefinition.length, - }) + } + const fn = vm.runInThisContext(code, options) await fn(...Object.values(context)) - - return exports } prepareContext(context: Record) { @@ -446,11 +434,15 @@ export class ViteNodeRunner { return !path.endsWith('.mjs') && 'default' in mod } + protected importExternalModule(path: string) { + return import(path) + } + /** * Import a module and interop it */ async interopedImport(path: string) { - const importedModule = await import(path) + const importedModule = await this.importExternalModule(path) if (!this.shouldInterop(path, importedModule)) return importedModule diff --git a/packages/vite-node/src/constants.ts b/packages/vite-node/src/constants.ts new file mode 100644 index 000000000000..9d098cb66fc4 --- /dev/null +++ b/packages/vite-node/src/constants.ts @@ -0,0 +1,38 @@ +export const KNOWN_ASSET_TYPES = [ + // images + 'apng', + 'png', + 'jpe?g', + 'jfif', + 'pjpeg', + 'pjp', + 'gif', + 'svg', + 'ico', + 'webp', + 'avif', + + // media + 'mp4', + 'webm', + 'ogg', + 'mp3', + 'wav', + 'flac', + 'aac', + + // fonts + 'woff2?', + 'eot', + 'ttf', + 'otf', + + // other + 'webmanifest', + 'pdf', + 'txt', +] + +export const KNOWN_ASSET_RE = new RegExp(`\\.(${KNOWN_ASSET_TYPES.join('|')})$`) +export const CSS_LANGS_RE + = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ diff --git a/packages/vite-node/src/externalize.ts b/packages/vite-node/src/externalize.ts index ba7bc1c124f0..7dcf090e3e92 100644 --- a/packages/vite-node/src/externalize.ts +++ b/packages/vite-node/src/externalize.ts @@ -3,41 +3,7 @@ import { isValidNodeImport } from 'mlly' import { join } from 'pathe' import type { DepsHandlingOptions } from './types' import { isNodeBuiltin, slash } from './utils' - -const KNOWN_ASSET_TYPES = [ - // images - 'apng', - 'png', - 'jpe?g', - 'jfif', - 'pjpeg', - 'pjp', - 'gif', - 'svg', - 'ico', - 'webp', - 'avif', - - // media - 'mp4', - 'webm', - 'ogg', - 'mp3', - 'wav', - 'flac', - 'aac', - - // fonts - 'woff2?', - 'eot', - 'ttf', - 'otf', - - // other - 'webmanifest', - 'pdf', - 'txt', -] +import { KNOWN_ASSET_TYPES } from './constants' const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/ const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/ diff --git a/packages/vite-node/src/utils.ts b/packages/vite-node/src/utils.ts index 1f2b76362cc3..fdd349db9291 100644 --- a/packages/vite-node/src/utils.ts +++ b/packages/vite-node/src/utils.ts @@ -1,7 +1,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { builtinModules } from 'node:module' import { existsSync } from 'node:fs' -import { resolve } from 'pathe' +import { dirname, resolve } from 'pathe' import type { Arrayable, Nullable } from './types' export const isWindows = process.platform === 'win32' @@ -153,3 +153,49 @@ export function toArray(array?: Nullable>): Array { return [array] } + +export function getCachedData( + cache: Map, + basedir: string, + originalBasedir: string, +) { + const pkgData = cache.get(getFnpdCacheKey(basedir)) + if (pkgData) { + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + cache.set(getFnpdCacheKey(dir), pkgData) + }) + return pkgData + } +} + +export function setCacheData( + cache: Map, + data: T, + basedir: string, + originalBasedir: string, +) { + cache.set(getFnpdCacheKey(basedir), data) + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + cache.set(getFnpdCacheKey(dir), data) + }) +} + +function getFnpdCacheKey(basedir: string) { + return `fnpd_${basedir}` +} + +/** + * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir. + * @param longerDir Longer dir path, e.g. `/User/foo/bar/baz` + * @param shorterDir Shorter dir path, e.g. `/User/foo` + */ +function traverseBetweenDirs( + longerDir: string, + shorterDir: string, + cb: (dir: string) => void, +) { + while (longerDir !== shorterDir) { + cb(longerDir) + longerDir = dirname(longerDir) + } +} diff --git a/packages/vitest/execute.d.ts b/packages/vitest/execute.d.ts new file mode 100644 index 000000000000..9901479f9a8e --- /dev/null +++ b/packages/vitest/execute.d.ts @@ -0,0 +1 @@ +export * from './dist/execute.js' diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 7d21ab7d2fb6..9cc26025a1a8 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -45,6 +45,10 @@ "types": "./dist/node.d.ts", "import": "./dist/node.js" }, + "./execute": { + "types": "./dist/execute.d.ts", + "import": "./dist/execute.js" + }, "./browser": { "types": "./dist/browser.d.ts", "import": "./dist/browser.js" diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 63d3cb65766b..3f7ab46a8b43 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -24,12 +24,15 @@ const entries = [ 'src/runners.ts', 'src/environments.ts', 'src/runtime/worker.ts', + 'src/runtime/vm.ts', 'src/runtime/child.ts', 'src/runtime/loader.ts', 'src/runtime/entry.ts', + 'src/runtime/entry-vm.ts', 'src/integrations/spy.ts', 'src/coverage.ts', 'src/public/utils.ts', + 'src/public/execute.ts', ] const dtsEntries = { @@ -52,6 +55,7 @@ const external = [ 'node:worker_threads', 'node:fs', 'rollup', + 'node:vm', 'inspector', 'webdriverio', 'safaridriver', @@ -59,6 +63,7 @@ const external = [ 'vite-node/source-map', 'vite-node/client', 'vite-node/server', + 'vite-node/constants', 'vite-node/utils', '@vitest/utils/diff', '@vitest/utils/error', diff --git a/packages/vitest/src/integrations/chai/config.ts b/packages/vitest/src/integrations/chai/config.ts new file mode 100644 index 000000000000..2adbcb6143f2 --- /dev/null +++ b/packages/vitest/src/integrations/chai/config.ts @@ -0,0 +1,7 @@ +import * as chai from 'chai' + +export function setupChaiConfig(config: ChaiConfig) { + Object.assign(chai.config, config) +} + +export type ChaiConfig = Omit, 'useProxy' | 'proxyExcludedKeys'> diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index 93d3c6d8094a..b25dc2f605a4 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -7,7 +7,8 @@ import { getCurrentTest } from '@vitest/runner' import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect' import type { Assertion, ExpectStatic } from '@vitest/expect' import type { MatcherState } from '../../types/chai' -import { getCurrentEnvironment, getFullName } from '../../utils' +import { getFullName } from '../../utils/tasks' +import { getCurrentEnvironment } from '../../utils/global' export function createExpect(test?: Test) { const expect = ((value: any, message?: string): Assertion => { @@ -95,9 +96,3 @@ Object.defineProperty(globalThis, GLOBAL_EXPECT, { export { assert, should } from 'chai' export { chai, globalExpect as expect } - -export function setupChaiConfig(config: ChaiConfig) { - Object.assign(chai.config, config) -} - -export type ChaiConfig = Omit, 'useProxy' | 'proxyExcludedKeys'> diff --git a/packages/vitest/src/integrations/env/edge-runtime.ts b/packages/vitest/src/integrations/env/edge-runtime.ts index 9d072eb0aaec..9aa3a0fb6cbf 100644 --- a/packages/vitest/src/integrations/env/edge-runtime.ts +++ b/packages/vitest/src/integrations/env/edge-runtime.ts @@ -5,6 +5,24 @@ import { populateGlobal } from './utils' export default ({ name: 'edge-runtime', transformMode: 'ssr', + async setupVM() { + const { EdgeVM } = await importModule('@edge-runtime/vm') as typeof import('@edge-runtime/vm') + const vm = new EdgeVM({ + extend: (context) => { + context.global = context + context.Buffer = Buffer + return context + }, + }) + return { + getVmContext() { + return vm.context + }, + teardown() { + // nothing to teardown + }, + } + }, async setup(global) { const { EdgeVM } = await importModule('@edge-runtime/vm') as typeof import('@edge-runtime/vm') const vm = new EdgeVM({ diff --git a/packages/vitest/src/integrations/env/happy-dom.ts b/packages/vitest/src/integrations/env/happy-dom.ts index 0a0cb1cc89b0..3df3b2bd3b5b 100644 --- a/packages/vitest/src/integrations/env/happy-dom.ts +++ b/packages/vitest/src/integrations/env/happy-dom.ts @@ -5,6 +5,27 @@ import { populateGlobal } from './utils' export default ({ name: 'happy-dom', transformMode: 'web', + async setupVM() { + const { Window } = await importModule('happy-dom') as typeof import('happy-dom') + const win = new Window() as any + + // TODO: browser doesn't expose Buffer, but a lot of dependencies use it + win.Buffer = Buffer + win.Uint8Array = Uint8Array + + // inject structuredClone if it exists + if (typeof structuredClone !== 'undefined' && !win.structuredClone) + win.structuredClone = structuredClone + + return { + getVmContext() { + return win + }, + teardown() { + win.happyDOM.cancelAsync() + }, + } + }, async setup(global) { // happy-dom v3 introduced a breaking change to Window, but // provides GlobalWindow as a way to use previous behaviour diff --git a/packages/vitest/src/integrations/env/index.ts b/packages/vitest/src/integrations/env/index.ts index 0cbd7261df8d..3015f9ddb252 100644 --- a/packages/vitest/src/integrations/env/index.ts +++ b/packages/vitest/src/integrations/env/index.ts @@ -1,5 +1,7 @@ +import { pathToFileURL } from 'node:url' +import { normalize, resolve } from 'pathe' +import { resolveModule } from 'local-pkg' import type { BuiltinEnvironment, VitestEnvironment } from '../../types/config' -import type { VitestExecutor } from '../../node' import type { Environment } from '../../types' import node from './node' import jsdom from './jsdom' @@ -33,16 +35,25 @@ export function getEnvPackageName(env: VitestEnvironment) { return `vitest-environment-${env}` } -export async function loadEnvironment(name: VitestEnvironment, executor: VitestExecutor): Promise { +export async function loadEnvironment(name: VitestEnvironment, root: string): Promise { if (isBuiltinEnvironment(name)) return environments[name] - const packageId = (name[0] === '.' || name[0] === '/') ? name : `vitest-environment-${name}` - const pkg = await executor.executeId(packageId) - if (!pkg || !pkg.default || typeof pkg.default !== 'object' || typeof pkg.default.setup !== 'function') { - throw new Error( + const packageId = name[0] === '.' || name[0] === '/' + ? resolve(root, name) + : resolveModule(`vitest-environment-${name}`, { paths: [root] }) ?? resolve(root, name) + const pkg = await import(pathToFileURL(normalize(packageId)).href) + if (!pkg || !pkg.default || typeof pkg.default !== 'object') { + throw new TypeError( `Environment "${name}" is not a valid environment. ` - + `Path "${packageId}" should export default object with a "setup" method.`, + + `Path "${packageId}" should export default object with a "setup" or/and "setupVM" method.`, ) } - return pkg.default + const environment = pkg.default + if (environment.transformMode !== 'web' && environment.transformMode !== 'ssr') { + throw new TypeError( + `Environment "${name}" is not a valid environment. ` + + `Path "${packageId}" should export default object with a "transformMode" method equal to "ssr" or "web".`, + ) + } + return environment } diff --git a/packages/vitest/src/integrations/env/jsdom.ts b/packages/vitest/src/integrations/env/jsdom.ts index f197e9c3444e..30910b082f28 100644 --- a/packages/vitest/src/integrations/env/jsdom.ts +++ b/packages/vitest/src/integrations/env/jsdom.ts @@ -29,6 +29,62 @@ function catchWindowErrors(window: Window) { export default ({ name: 'jsdom', transformMode: 'web', + async setupVM({ jsdom = {} }) { + const { + CookieJar, + JSDOM, + ResourceLoader, + VirtualConsole, + } = await importModule('jsdom') as typeof import('jsdom') + const { + html = '', + userAgent, + url = 'http://localhost:3000', + contentType = 'text/html', + pretendToBeVisual = true, + includeNodeLocations = false, + runScripts = 'dangerously', + resources, + console = false, + cookieJar = false, + ...restOptions + } = jsdom as any + const dom = new JSDOM( + html, + { + pretendToBeVisual, + resources: resources ?? (userAgent ? new ResourceLoader({ userAgent }) : undefined), + runScripts, + url, + virtualConsole: (console && globalThis.console) ? new VirtualConsole().sendTo(globalThis.console) : undefined, + cookieJar: cookieJar ? new CookieJar() : undefined, + includeNodeLocations, + contentType, + userAgent, + ...restOptions, + }, + ) + const clearWindowErrors = catchWindowErrors(dom.window as any) + + // TODO: browser doesn't expose Buffer, but a lot of dependencies use it + dom.window.Buffer = Buffer + // Buffer extends Uint8Array + dom.window.Uint8Array = Uint8Array + + // inject structuredClone if it exists + if (typeof structuredClone !== 'undefined' && !dom.window.structuredClone) + dom.window.structuredClone = structuredClone + + return { + getVmContext() { + return dom.getInternalVMContext() + }, + teardown() { + clearWindowErrors() + dom.window.close() + }, + } + }, async setup(global, { jsdom = {} }) { const { CookieJar, diff --git a/packages/vitest/src/integrations/env/node.ts b/packages/vitest/src/integrations/env/node.ts index ef1a01cb1523..4ba49e471748 100644 --- a/packages/vitest/src/integrations/env/node.ts +++ b/packages/vitest/src/integrations/env/node.ts @@ -1,9 +1,117 @@ import { Console } from 'node:console' import type { Environment } from '../../types' +// some globals we do not want, either because deprecated or we set it ourselves +const denyList = new Set([ + 'GLOBAL', + 'root', + 'global', + 'Buffer', + 'ArrayBuffer', + 'Uint8Array', +]) + +const nodeGlobals = new Map( + Object.getOwnPropertyNames(globalThis) + .filter(global => !denyList.has(global)) + .map((nodeGlobalsKey) => { + const descriptor = Object.getOwnPropertyDescriptor( + globalThis, + nodeGlobalsKey, + ) + + if (!descriptor) { + throw new Error( + `No property descriptor for ${nodeGlobalsKey}, this is a bug in Vitest.`, + ) + } + + return [nodeGlobalsKey, descriptor] + }), +) + export default ({ name: 'node', transformMode: 'ssr', + // this is largely copied from jest's node environment + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + const global = vm.runInContext( + 'this', + context, + ) + + const contextGlobals = new Set(Object.getOwnPropertyNames(global)) + for (const [nodeGlobalsKey, descriptor] of nodeGlobals) { + if (!contextGlobals.has(nodeGlobalsKey)) { + if (descriptor.configurable) { + Object.defineProperty(global, nodeGlobalsKey, { + configurable: true, + enumerable: descriptor.enumerable, + get() { + // @ts-expect-error: no index signature + const val = globalThis[nodeGlobalsKey] as unknown + + // override lazy getter + Object.defineProperty(global, nodeGlobalsKey, { + configurable: true, + enumerable: descriptor.enumerable, + value: val, + writable: + descriptor.writable === true + // Node 19 makes performance non-readable. This is probably not the correct solution. + || nodeGlobalsKey === 'performance', + }) + return val + }, + set(val: unknown) { + // override lazy getter + Object.defineProperty(global, nodeGlobalsKey, { + configurable: true, + enumerable: descriptor.enumerable, + value: val, + writable: true, + }) + }, + }) + } + else if ('value' in descriptor) { + Object.defineProperty(global, nodeGlobalsKey, { + configurable: false, + enumerable: descriptor.enumerable, + value: descriptor.value, + writable: descriptor.writable, + }) + } + else { + Object.defineProperty(global, nodeGlobalsKey, { + configurable: false, + enumerable: descriptor.enumerable, + get: descriptor.get, + set: descriptor.set, + }) + } + } + } + + global.global = global + global.Buffer = Buffer + global.ArrayBuffer = ArrayBuffer + // TextEncoder (global or via 'util') references a Uint8Array constructor + // different than the global one used by users in tests. This makes sure the + // same constructor is referenced by both. + global.Uint8Array = Uint8Array + + return { + getVmContext() { + return context + }, + teardown() { + // + }, + } + }, async setup(global) { global.console.Console = Console return { diff --git a/packages/vitest/src/integrations/run-once.ts b/packages/vitest/src/integrations/run-once.ts index 52e391a47d04..be150ed08904 100644 --- a/packages/vitest/src/integrations/run-once.ts +++ b/packages/vitest/src/integrations/run-once.ts @@ -1,4 +1,4 @@ -import { getWorkerState } from '../utils' +import { getWorkerState } from '../utils/global' const filesCount = new Map() const cache = new Map() diff --git a/packages/vitest/src/integrations/snapshot/environments/node.ts b/packages/vitest/src/integrations/snapshot/environments/node.ts index 59657bb79e13..739501a67f29 100644 --- a/packages/vitest/src/integrations/snapshot/environments/node.ts +++ b/packages/vitest/src/integrations/snapshot/environments/node.ts @@ -1,12 +1,16 @@ import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment' -import { rpc } from '../../../runtime/rpc' +import type { WorkerRPC } from '../../../types/worker' export class VitestSnapshotEnvironment extends NodeSnapshotEnvironment { + constructor(private rpc: WorkerRPC) { + super() + } + getHeader(): string { return `// Vitest Snapshot v${this.getVersion()}, https://vitest.dev/guide/snapshot.html` } resolvePath(filepath: string): Promise { - return rpc().resolveSnapshotPath(filepath) + return this.rpc.resolveSnapshotPath(filepath) } } diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 1fcc6f30d553..a93cab706786 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -3,8 +3,9 @@ import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { parseSingleStack } from '../utils/source-map' import type { VitestMocker } from '../runtime/mocker' import type { ResolvedConfig, RuntimeConfig } from '../types' -import { getWorkerState, resetModules, waitForImportsToResolve } from '../utils' import type { MockFactoryWithHelper } from '../types/mocker' +import { getWorkerState } from '../utils/global' +import { resetModules, waitForImportsToResolve } from '../utils/modules' import { FakeTimers } from './mock/timers' import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy' import { fn, isMockFunction, spies, spyOn } from './spy' @@ -163,10 +164,10 @@ function createVitest(): VitestUtils { // @ts-expect-error injected by vite-nide ? __vitest_mocker__ : new Proxy({}, { - get(name) { + get(_, name) { throw new Error( 'Vitest mocker was not initialized in this environment. ' - + `vi.${name}() is forbidden.`, + + `vi.${String(name)}() is forbidden.`, ) }, }) diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index a6456138948f..0592f1ecbd66 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -56,6 +56,9 @@ export async function startVitest( if (typeof options.browser === 'object' && !('enabled' in options.browser)) options.browser.enabled = true + if ('threads' in options && options.experimentalVmThreads) + throw new Error('Cannot use both "threads" (or "no-threads") and "experimentalVmThreads" at the same time.') + const ctx = await createVitest(mode, options, viteOverrides) if (mode === 'test' && ctx.config.coverage.enabled) { diff --git a/packages/vitest/src/node/cli.ts b/packages/vitest/src/node/cli.ts index c4573ed3ce3b..25872408d6b7 100644 --- a/packages/vitest/src/node/cli.ts +++ b/packages/vitest/src/node/cli.ts @@ -23,6 +23,8 @@ cli .option('--api [api]', 'Serve API, available options: --api.port , --api.host [host] and --api.strictPort') .option('--threads', 'Enabled threads (default: true)') .option('--single-thread', 'Run tests inside a single thread, requires --threads (default: false)') + .option('--experimental-vm-threads', 'Run tests in a worker pool using VM isolation (default: false)') + .option('--experimental-vm-worker-memory-limit', 'Set the maximum allowed memory for a worker. When reached, a new worker will be created instead') .option('--silent', 'Silent console output from tests') .option('--hideSkippedTests', 'Hide logs for skipped tests') .option('--isolate', 'Isolate environment for each test file (default: true)') diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 50ea5fd85c31..169c1f36a8e0 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -1,12 +1,13 @@ +import { totalmem } from 'node:os' import { resolveModule } from 'local-pkg' import { normalize, relative, resolve } from 'pathe' import c from 'picocolors' import type { ResolvedConfig as ResolvedViteConfig } from 'vite' - import type { ApiConfig, ResolvedConfig, UserConfig, VitestRunMode } from '../types' import { defaultBrowserPort, defaultPort } from '../constants' import { benchmarkConfigDefaults, configDefaults } from '../defaults' import { isCI, toArray } from '../utils' +import { getWorkerMemoryLimit, stringToBytes } from '../utils/memory-limit' import { VitestCache } from './cache' import { BaseSequencer } from './sequencers/BaseSequencer' import { RandomSequencer } from './sequencers/RandomSequencer' @@ -206,6 +207,11 @@ export function resolveConfig( snapshotEnvironment: null as any, } + resolved.experimentalVmWorkerMemoryLimit = stringToBytes( + getWorkerMemoryLimit(resolved), + totalmem(), + ) + if (options.resolveSnapshotPath) delete (resolved as UserConfig).resolveSnapshotPath diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 80b128a4d1a3..bddc19919dba 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -6,8 +6,5 @@ export { startVitest } from './cli-api' export { registerConsoleShortcuts } from './stdin' export type { WorkspaceSpec } from './pool' -export { VitestExecutor } from '../runtime/execute' -export type { ExecuteOptions } from '../runtime/execute' - export type { TestSequencer, TestSequencerConstructor } from './sequencers/types' export { BaseSequencer } from './sequencers/BaseSequencer' diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index dcfa13a14bd6..329f16888309 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -7,6 +7,7 @@ import type { Vitest } from './core' import { createChildProcessPool } from './pools/child' import { createThreadsPool } from './pools/threads' import { createBrowserPool } from './pools/browser' +import { createVmThreadsPool } from './pools/vm-threads' import type { WorkspaceProject } from './workspace' export type WorkspaceSpec = [project: WorkspaceProject, testFile: string] @@ -30,11 +31,14 @@ export function createPool(ctx: Vitest): ProcessPool { child_process: null, threads: null, browser: null, + experimentalVmThreads: null, } - function getDefaultPoolName(project: WorkspaceProject) { + function getDefaultPoolName(project: WorkspaceProject): VitestPool { if (project.config.browser.enabled) return 'browser' + if (project.config.experimentalVmThreads) + return 'experimentalVmThreads' if (project.config.threads) return 'threads' return 'child_process' @@ -67,6 +71,7 @@ export function createPool(ctx: Vitest): ProcessPool { suppressLoaderWarningsPath, '--experimental-loader', loaderPath, + ...conditions, ] : [ ...execArgv, @@ -86,10 +91,13 @@ export function createPool(ctx: Vitest): ProcessPool { child_process: [] as WorkspaceSpec[], threads: [] as WorkspaceSpec[], browser: [] as WorkspaceSpec[], + experimentalVmThreads: [] as WorkspaceSpec[], } for (const spec of files) { const pool = getPoolName(spec) + if (!(pool in filesByPool)) + throw new Error(`Unknown pool name "${pool}" for ${spec[1]}. Available pools: ${Object.keys(filesByPool).join(', ')}`) filesByPool[pool].push(spec) } @@ -102,6 +110,11 @@ export function createPool(ctx: Vitest): ProcessPool { return pools.browser.runTests(files, invalidate) } + if (pool === 'experimentalVmThreads') { + pools.experimentalVmThreads ??= createVmThreadsPool(ctx, options) + return pools.experimentalVmThreads.runTests(files, invalidate) + } + if (pool === 'threads') { pools.threads ??= createThreadsPool(ctx, options) return pools.threads.runTests(files, invalidate) diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index 8ba9fe620c67..a22e91fb3c12 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -72,6 +72,8 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce const child = fork(childPath, [], { execArgv, env, + // TODO: investigate + // serialization: 'advanced', }) children.add(child) setupChildProcessChannel(project, child) diff --git a/packages/vitest/src/node/pools/vm-threads.ts b/packages/vitest/src/node/pools/vm-threads.ts new file mode 100644 index 000000000000..5cc867834728 --- /dev/null +++ b/packages/vitest/src/node/pools/vm-threads.ts @@ -0,0 +1,156 @@ +import { MessageChannel } from 'node:worker_threads' +import { cpus } from 'node:os' +import { pathToFileURL } from 'node:url' +import { createBirpc } from 'birpc' +import { resolve } from 'pathe' +import type { Options as TinypoolOptions } from 'tinypool' +import Tinypool from 'tinypool' +import { distDir, rootDir } from '../../paths' +import type { ContextTestEnvironment, ResolvedConfig, RunnerRPC, RuntimeRPC, Vitest, WorkerContext } from '../../types' +import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' +import { groupFilesByEnv } from '../../utils/test-helpers' +import { AggregateError } from '../../utils/base' +import type { WorkspaceProject } from '../workspace' +import { createMethodsRPC } from './rpc' + +const workerPath = pathToFileURL(resolve(distDir, './vm.js')).href +const suppressLoaderWarningsPath = resolve(rootDir, './suppress-warnings.cjs') + +function createWorkerChannel(project: WorkspaceProject) { + const channel = new MessageChannel() + const port = channel.port2 + const workerPort = channel.port1 + + const rpc = createBirpc( + createMethodsRPC(project), + { + eventNames: ['onCancel'], + post(v) { + port.postMessage(v) + }, + on(fn) { + port.on('message', fn) + }, + }, + ) + + project.ctx.onCancel(reason => rpc.onCancel(reason)) + + return { workerPort, port } +} + +export function createVmThreadsPool(ctx: Vitest, { execArgv, env }: PoolProcessOptions): ProcessPool { + const threadsCount = ctx.config.watch + ? Math.max(Math.floor(cpus().length / 2), 1) + : Math.max(cpus().length - 1, 1) + + const maxThreads = ctx.config.maxThreads ?? threadsCount + const minThreads = ctx.config.minThreads ?? threadsCount + + const options: TinypoolOptions = { + filename: workerPath, + // TODO: investigate further + // It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191 + useAtomics: ctx.config.useAtomics ?? false, + + maxThreads, + minThreads, + + env, + execArgv: [ + '--experimental-import-meta-resolve', + '--experimental-vm-modules', + '--require', + suppressLoaderWarningsPath, + ...execArgv, + ], + + terminateTimeout: ctx.config.teardownTimeout, + } + + if (ctx.config.singleThread) { + options.concurrentTasksPerWorker = 1 + options.maxThreads = 1 + options.minThreads = 1 + } + + const pool = new Tinypool(options) + + const runWithFiles = (name: string): RunWithFiles => { + let id = 0 + + async function runFiles(project: WorkspaceProject, config: ResolvedConfig, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { + ctx.state.clearFiles(project, files) + const { workerPort, port } = createWorkerChannel(project) + const workerId = ++id + const data: WorkerContext = { + port: workerPort, + config, + files, + invalidates, + environment, + workerId, + } + try { + await pool.run(data, { transferList: [workerPort], name }) + } + catch (error) { + // Worker got stuck and won't terminate - this may cause process to hang + if (error instanceof Error && /Failed to terminate worker/.test(error.message)) + ctx.state.addProcessTimeoutCause(`Failed to terminate worker while running ${files.join(', ')}.`) + + // Intentionally cancelled + else if (ctx.isCancelling && error instanceof Error && /The task has been cancelled/.test(error.message)) + ctx.state.cancelFiles(files, ctx.config.root) + + else + throw error + } + finally { + port.close() + workerPort.close() + } + } + + const Sequencer = ctx.config.sequence.sequencer + const sequencer = new Sequencer(ctx) + + return async (specs, invalidates) => { + const configs = new Map() + const getConfig = (project: WorkspaceProject): ResolvedConfig => { + if (configs.has(project)) + return configs.get(project)! + + const config = project.getSerializableConfig() + configs.set(project, config) + return config + } + + const { shard } = ctx.config + + if (shard) + specs = await sequencer.shard(specs) + + specs = await sequencer.sort(specs) + + const filesByEnv = await groupFilesByEnv(specs) + const promises = Object.values(filesByEnv).flat() + const results = await Promise.allSettled(promises + .map(({ file, environment, project }) => runFiles(project, getConfig(project), [file], environment, invalidates))) + + const errors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason) + if (errors.length > 0) + throw new AggregateError(errors, 'Errors occurred while running tests. For more information, see serialized error.') + } + } + + return { + runTests: runWithFiles('run'), + close: async () => { + // node before 16.17 has a bug that causes FATAL ERROR because of the race condition + const nodeVersion = Number(process.version.match(/v(\d+)\.(\d+)/)?.[0].slice(1)) + if (nodeVersion >= 16.17) + await pool.destroy() + }, + } +} diff --git a/packages/vitest/src/public/execute.ts b/packages/vitest/src/public/execute.ts new file mode 100644 index 000000000000..05fef48145a4 --- /dev/null +++ b/packages/vitest/src/public/execute.ts @@ -0,0 +1 @@ +export { VitestExecutor } from '../runtime/execute' diff --git a/packages/vitest/src/runtime/child.ts b/packages/vitest/src/runtime/child.ts index 3dcd5874acfe..5bdf6cd984ad 100644 --- a/packages/vitest/src/runtime/child.ts +++ b/packages/vitest/src/runtime/child.ts @@ -3,15 +3,16 @@ import v8 from 'node:v8' import { createBirpc } from 'birpc' import { parseRegexp } from '@vitest/utils' import type { CancelReason } from '@vitest/runner' -import type { ResolvedConfig } from '../types' +import type { ResolvedConfig, WorkerGlobalState } from '../types' import type { RunnerRPC, RuntimeRPC } from '../types/rpc' import type { ChildContext } from '../types/child' +import { loadEnvironment } from '../integrations/env' import { mockMap, moduleCache, startViteNode } from './execute' -import { rpcDone } from './rpc' +import { createSafeRpc, rpcDone } from './rpc' import { setupInspect } from './inspector' -function init(ctx: ChildContext) { - const { config, environment } = ctx +async function init(ctx: ChildContext) { + const { config } = ctx process.env.VITEST_WORKER_ID = '1' process.env.VITEST_POOL_ID = '1' @@ -21,35 +22,42 @@ function init(ctx: ChildContext) { setCancel = resolve }) - // @ts-expect-error untyped global - globalThis.__vitest_environment__ = environment.name - // @ts-expect-error I know what I am doing :P - globalThis.__vitest_worker__ = { + const rpc = createBirpc( + { + onCancel: setCancel, + }, + { + eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onWorkerExit', 'onCancel'], + serialize: v8.serialize, + deserialize: v => v8.deserialize(Buffer.from(v)), + post(v) { + process.send?.(v) + }, + on(fn) { process.on('message', fn) }, + }, + ) + + const environment = await loadEnvironment(ctx.environment.name, ctx.config.root) + if (ctx.environment.transformMode) + environment.transformMode = ctx.environment.transformMode + + const state: WorkerGlobalState = { ctx, moduleCache, config, mockMap, onCancel, + environment, durations: { environment: 0, prepare: performance.now(), }, - rpc: createBirpc( - { - onCancel: setCancel, - }, - { - eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onWorkerExit', 'onCancel'], - serialize: v8.serialize, - deserialize: v => v8.deserialize(Buffer.from(v)), - post(v) { - process.send?.(v) - }, - on(fn) { process.on('message', fn) }, - }, - ), + rpc: createSafeRpc(rpc), } + // @ts-expect-error I know what I am doing :P + globalThis.__vitest_worker__ = state + if (ctx.invalidates) { ctx.invalidates.forEach((fsPath) => { moduleCache.delete(fsPath) @@ -57,6 +65,8 @@ function init(ctx: ChildContext) { }) } ctx.files.forEach(i => moduleCache.delete(i)) + + return state } function parsePossibleRegexp(str: string | RegExp) { @@ -76,9 +86,11 @@ export async function run(ctx: ChildContext) { const inspectorCleanup = setupInspect(ctx.config) try { - init(ctx) - const { run, executor, environment } = await startViteNode(ctx) - await run(ctx.files, ctx.config, { ...ctx.environment, environment }, executor) + const state = await init(ctx) + const { run, executor } = await startViteNode({ + state, + }) + await run(ctx.files, ctx.config, { environment: state.environment, options: ctx.environment.options }, executor) await rpcDone() } finally { diff --git a/packages/vitest/src/runtime/console.ts b/packages/vitest/src/runtime/console.ts new file mode 100644 index 000000000000..3061221f7d55 --- /dev/null +++ b/packages/vitest/src/runtime/console.ts @@ -0,0 +1,112 @@ +import { Writable } from 'node:stream' +import { Console } from 'node:console' +import { getSafeTimers } from '@vitest/utils' +import { RealDate } from '../integrations/mock/date' +import type { WorkerGlobalState } from '../types' + +export function createCustomConsole(state: WorkerGlobalState) { + const stdoutBuffer = new Map() + const stderrBuffer = new Map() + const timers = new Map() + const unknownTestId = '__vitest__unknown_test__' + + const { setTimeout, clearTimeout } = getSafeTimers() + + // group sync console.log calls with macro task + function schedule(taskId: string) { + const timer = timers.get(taskId)! + const { stdoutTime, stderrTime } = timer + clearTimeout(timer.timer) + timer.timer = setTimeout(() => { + if (stderrTime < stdoutTime) { + sendStderr(taskId) + sendStdout(taskId) + } + else { + sendStdout(taskId) + sendStderr(taskId) + } + }) + } + function sendStdout(taskId: string) { + const buffer = stdoutBuffer.get(taskId) + if (!buffer) + return + const content = buffer.map(i => String(i)).join('') + const timer = timers.get(taskId)! + state.rpc.onUserConsoleLog({ + type: 'stdout', + content: content || '', + taskId, + time: timer.stdoutTime || RealDate.now(), + size: buffer.length, + }) + stdoutBuffer.set(taskId, []) + timer.stdoutTime = 0 + } + function sendStderr(taskId: string) { + const buffer = stderrBuffer.get(taskId) + if (!buffer) + return + const content = buffer.map(i => String(i)).join('') + const timer = timers.get(taskId)! + state.rpc.onUserConsoleLog({ + type: 'stderr', + content: content || '', + taskId, + time: timer.stderrTime || RealDate.now(), + size: buffer.length, + }) + stderrBuffer.set(taskId, []) + timer.stderrTime = 0 + } + + const stdout = new Writable({ + write(data, encoding, callback) { + const id = state?.current?.id ?? unknownTestId + let timer = timers.get(id) + if (timer) { + timer.stdoutTime = timer.stdoutTime || RealDate.now() + } + else { + timer = { stdoutTime: RealDate.now(), stderrTime: RealDate.now(), timer: 0 } + timers.set(id, timer) + } + let buffer = stdoutBuffer.get(id) + if (!buffer) { + buffer = [] + stdoutBuffer.set(id, buffer) + } + buffer.push(data) + schedule(id) + callback() + }, + }) + const stderr = new Writable({ + write(data, encoding, callback) { + const id = state?.current?.id ?? unknownTestId + let timer = timers.get(id) + if (timer) { + timer.stderrTime = timer.stderrTime || RealDate.now() + } + else { + timer = { stderrTime: RealDate.now(), stdoutTime: RealDate.now(), timer: 0 } + timers.set(id, timer) + } + let buffer = stderrBuffer.get(id) + if (!buffer) { + buffer = [] + stderrBuffer.set(id, buffer) + } + buffer.push(data) + schedule(id) + callback() + }, + }) + return new Console({ + stdout, + stderr, + colorMode: true, + groupIndentation: 2, + }) +} diff --git a/packages/vitest/src/runtime/entry-vm.ts b/packages/vitest/src/runtime/entry-vm.ts new file mode 100644 index 000000000000..78edc1a32e64 --- /dev/null +++ b/packages/vitest/src/runtime/entry-vm.ts @@ -0,0 +1,57 @@ +import { isatty } from 'node:tty' +import { createRequire } from 'node:module' +import { performance } from 'node:perf_hooks' +import { startTests } from '@vitest/runner' +import { createColors, setupColors } from '@vitest/utils' +import { setupChaiConfig } from '../integrations/chai/config' +import { startCoverageInsideWorker, stopCoverageInsideWorker } from '../integrations/coverage' +import type { ResolvedConfig } from '../types' +import { getWorkerState } from '../utils/global' +import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node' +import * as VitestIndex from '../index' +import type { VitestExecutor } from './execute' +import { resolveTestRunner } from './runners' +import { setupCommonEnv } from './setup.common' + +export async function run(files: string[], config: ResolvedConfig, executor: VitestExecutor): Promise { + const workerState = getWorkerState() + + await setupCommonEnv(config) + + Object.defineProperty(globalThis, '__vitest_index__', { + value: VitestIndex, + enumerable: false, + }) + + config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(workerState.rpc) + + setupColors(createColors(isatty(1))) + + if (workerState.environment.transformMode === 'web') { + const _require = createRequire(import.meta.url) + // always mock "required" `css` files, because we cannot process them + _require.extensions['.css'] = () => ({}) + _require.extensions['.scss'] = () => ({}) + _require.extensions['.sass'] = () => ({}) + _require.extensions['.less'] = () => ({}) + } + + await startCoverageInsideWorker(config.coverage, executor) + + if (config.chaiConfig) + setupChaiConfig(config.chaiConfig) + + const runner = await resolveTestRunner(config, executor) + + workerState.durations.prepare = performance.now() - workerState.durations.prepare + + for (const file of files) { + workerState.filepath = file + + await startTests([file], runner) + + workerState.filepath = undefined + } + + await stopCoverageInsideWorker(config.coverage, executor) +} diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index a2c9edaeaa76..0dffd3f2b648 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -1,110 +1,30 @@ import { performance } from 'node:perf_hooks' -import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner' import { startTests } from '@vitest/runner' -import { resolve } from 'pathe' import type { ResolvedConfig, ResolvedTestEnvironment } from '../types' import { getWorkerState, resetModules } from '../utils' import { vi } from '../integrations/vi' -import { distDir } from '../paths' -import { startCoverageInsideWorker, stopCoverageInsideWorker, takeCoverageInsideWorker } from '../integrations/coverage' -import { setupChaiConfig } from '../integrations/chai' +import { startCoverageInsideWorker, stopCoverageInsideWorker } from '../integrations/coverage' +import { setupChaiConfig } from '../integrations/chai/config' import { setupGlobalEnv, withEnv } from './setup.node' -import { rpc } from './rpc' import type { VitestExecutor } from './execute' - -const runnersFile = resolve(distDir, 'runners.js') - -async function getTestRunnerConstructor(config: ResolvedConfig, executor: VitestExecutor): Promise { - if (!config.runner) { - const { VitestTestRunner, NodeBenchmarkRunner } = await executor.executeFile(runnersFile) - return (config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner) as VitestRunnerConstructor - } - const mod = await executor.executeId(config.runner) - if (!mod.default && typeof mod.default !== 'function') - throw new Error(`Runner must export a default function, but got ${typeof mod.default} imported from ${config.runner}`) - return mod.default as VitestRunnerConstructor -} - -async function getTestRunner(config: ResolvedConfig, executor: VitestExecutor): Promise { - const TestRunner = await getTestRunnerConstructor(config, executor) - const testRunner = new TestRunner(config) - - // inject private executor to every runner - Object.defineProperty(testRunner, '__vitest_executor', { - value: executor, - enumerable: false, - configurable: false, - }) - - if (!testRunner.config) - testRunner.config = config - - if (!testRunner.importFile) - throw new Error('Runner must implement "importFile" method.') - - // patch some methods, so custom runners don't need to call RPC - const originalOnTaskUpdate = testRunner.onTaskUpdate - testRunner.onTaskUpdate = async (task) => { - const p = rpc().onTaskUpdate(task) - await originalOnTaskUpdate?.call(testRunner, task) - return p - } - - const originalOnCollected = testRunner.onCollected - testRunner.onCollected = async (files) => { - const state = getWorkerState() - files.forEach((file) => { - file.prepareDuration = state.durations.prepare - file.environmentLoad = state.durations.environment - // should be collected only for a single test file in a batch - state.durations.prepare = 0 - state.durations.environment = 0 - }) - rpc().onCollected(files) - await originalOnCollected?.call(testRunner, files) - } - - const originalOnAfterRun = testRunner.onAfterRun - testRunner.onAfterRun = async (files) => { - const coverage = await takeCoverageInsideWorker(config.coverage, executor) - rpc().onAfterSuiteRun({ coverage }) - await originalOnAfterRun?.call(testRunner, files) - } - - const originalOnAfterRunTest = testRunner.onAfterRunTest - testRunner.onAfterRunTest = async (test) => { - if (config.bail && test.result?.state === 'fail') { - const previousFailures = await rpc().getCountOfFailedTests() - const currentFailures = 1 + previousFailures - - if (currentFailures >= config.bail) { - rpc().onCancel('test-failure') - testRunner.onCancel?.('test-failure') - } - } - await originalOnAfterRunTest?.call(testRunner, test) - } - - return testRunner -} +import { resolveTestRunner } from './runners' // browser shouldn't call this! export async function run(files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor): Promise { const workerState = getWorkerState() - await setupGlobalEnv(config) + await setupGlobalEnv(config, environment) await startCoverageInsideWorker(config.coverage, executor) if (config.chaiConfig) setupChaiConfig(config.chaiConfig) - const runner = await getTestRunner(config, executor) + const runner = await resolveTestRunner(config, executor) workerState.onCancel.then(reason => runner.onCancel?.(reason)) workerState.durations.prepare = performance.now() - workerState.durations.prepare - // @ts-expect-error untyped global - globalThis.__vitest_environment__ = environment.name + workerState.environment = environment.environment workerState.durations.environment = performance.now() diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index cbce90c327e6..1617a4793051 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -1,28 +1,32 @@ import { pathToFileURL } from 'node:url' -import { ModuleCacheMap, ViteNodeRunner } from 'vite-node/client' +import vm from 'node:vm' +import { DEFAULT_REQUEST_STUBS, ModuleCacheMap, ViteNodeRunner } from 'vite-node/client' import { isInternalRequest, isNodeBuiltin, isPrimitive } from 'vite-node/utils' import type { ViteNodeRunnerOptions } from 'vite-node' import { normalize, relative, resolve } from 'pathe' import { processError } from '@vitest/utils/error' import type { MockMap } from '../types/mocker' -import { getCurrentEnvironment, getWorkerState } from '../utils/global' -import type { ContextRPC, Environment, ResolvedConfig, ResolvedTestEnvironment } from '../types' +import type { ResolvedConfig, ResolvedTestEnvironment, WorkerGlobalState } from '../types' import { distDir } from '../paths' -import { loadEnvironment } from '../integrations/env' +import { getWorkerState } from '../utils/global' import { VitestMocker } from './mocker' -import { rpc } from './rpc' +import { ExternalModulesExecutor } from './external-executor' const entryUrl = pathToFileURL(resolve(distDir, 'entry.js')).href export interface ExecuteOptions extends ViteNodeRunnerOptions { mockMap: MockMap + packageCache: Map moduleDirectories?: string[] + context?: vm.Context + state: WorkerGlobalState } export async function createVitestExecutor(options: ExecuteOptions) { const runner = new VitestExecutor(options) await runner.executeId('/@vite/env') + await runner.mocker.initializeSpyModule() return runner } @@ -30,17 +34,35 @@ export async function createVitestExecutor(options: ExecuteOptions) { let _viteNode: { run: (files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor) => Promise executor: VitestExecutor - environment: Environment } +export const packageCache = new Map() export const moduleCache = new ModuleCacheMap() export const mockMap: MockMap = new Map() -export async function startViteNode(ctx: ContextRPC) { +export async function startViteNode(options: ContextExecutorOptions) { if (_viteNode) return _viteNode - const { config } = ctx + const executor = await startVitestExecutor(options) + + const { run } = await import(entryUrl) + + _viteNode = { run, executor } + + return _viteNode +} + +export interface ContextExecutorOptions { + mockMap?: MockMap + moduleCache?: ModuleCacheMap + context?: vm.Context + state: WorkerGlobalState +} + +export async function startVitestExecutor(options: ContextExecutorOptions) { + const state = () => getWorkerState() || options.state + const rpc = () => state().rpc const processExit = process.exit @@ -51,12 +73,12 @@ export async function startViteNode(ctx: ContextRPC) { } function catchError(err: unknown, type: string) { - const worker = getWorkerState() + const worker = state() const error = processError(err) if (!isPrimitive(error)) { error.VITEST_TEST_NAME = worker.current?.name if (worker.filepath) - error.VITEST_TEST_PATH = relative(config.root, worker.filepath) + error.VITEST_TEST_PATH = relative(state().config.root, worker.filepath) error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun } rpc().onUnhandledError(error, type) @@ -65,56 +87,118 @@ export async function startViteNode(ctx: ContextRPC) { process.on('uncaughtException', e => catchError(e, 'Uncaught Exception')) process.on('unhandledRejection', e => catchError(e, 'Unhandled Rejection')) - let transformMode: 'ssr' | 'web' = ctx.environment.transformMode ?? 'ssr' + const getTransformMode = () => { + return state().environment.transformMode ?? 'ssr' + } - const executor = await createVitestExecutor({ + return await createVitestExecutor({ fetchModule(id) { - return rpc().fetch(id, transformMode) + return rpc().fetch(id, getTransformMode()) }, resolveId(id, importer) { - return rpc().resolveId(id, importer, transformMode) + return rpc().resolveId(id, importer, getTransformMode()) }, + packageCache, moduleCache, mockMap, - interopDefault: config.deps.interopDefault, - moduleDirectories: config.deps.moduleDirectories, - root: config.root, - base: config.base, + get interopDefault() { return state().config.deps.interopDefault }, + get moduleDirectories() { return state().config.deps.moduleDirectories }, + get root() { return state().config.root }, + get base() { return state().config.base }, + ...options, }) +} - const environment = await loadEnvironment(ctx.environment.name, executor) - ctx.environment.environment = environment - transformMode = ctx.environment.transformMode ?? environment.transformMode ?? 'ssr' +function updateStyle(id: string, css: string) { + if (typeof document === 'undefined') + return - const { run } = await import(entryUrl) + const element = document.querySelector(`[data-vite-dev-id="${id}"]`) + if (element) { + element.textContent = css + return + } - _viteNode = { run, executor, environment } + const head = document.querySelector('head') + const style = document.createElement('style') + style.setAttribute('type', 'text/css') + style.setAttribute('data-vite-dev-id', id) + style.textContent = css + head?.appendChild(style) +} - return _viteNode +function removeStyle(id: string) { + if (typeof document === 'undefined') + return + const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`) + if (sheet) + document.head.removeChild(sheet) } export class VitestExecutor extends ViteNodeRunner { public mocker: VitestMocker + public externalModules?: ExternalModulesExecutor + + private primitives: { + Object: typeof Object + Reflect: typeof Reflect + Symbol: typeof Symbol + } constructor(public options: ExecuteOptions) { super(options) this.mocker = new VitestMocker(this) - Object.defineProperty(globalThis, '__vitest_mocker__', { - value: this.mocker, - writable: true, - configurable: true, - }) + if (!options.context) { + Object.defineProperty(globalThis, '__vitest_mocker__', { + value: this.mocker, + writable: true, + configurable: true, + }) + const clientStub = { ...DEFAULT_REQUEST_STUBS['@vite/client'], updateStyle, removeStyle } + this.options.requestStubs = { + '/@vite/client': clientStub, + '@vite/client': clientStub, + } + this.primitives = { + Object, + Reflect, + Symbol, + } + } + else { + this.externalModules = new ExternalModulesExecutor({ + context: options.context, + packageCache: options.packageCache, + }) + const clientStub = vm.runInContext( + `(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`, + options.context, + )(DEFAULT_REQUEST_STUBS['@vite/client']) + this.options.requestStubs = { + '/@vite/client': clientStub, + '@vite/client': clientStub, + } + this.primitives = vm.runInContext('({ Object, Reflect, Symbol })', options.context) + } + } + + protected getContextPrimitives() { + return this.primitives + } + + get state() { + return getWorkerState() || this.options.state } shouldResolveId(id: string, _importee?: string | undefined): boolean { if (isInternalRequest(id) || id.startsWith('data:')) return false - const environment = getCurrentEnvironment() + const transformMode = this.state.environment?.transformMode ?? 'ssr' // do not try and resolve node builtins in Node // import('url') returns Node internal even if 'url' package is installed - return environment === 'node' ? !isNodeBuiltin(id) : !id.startsWith('node:') + return transformMode === 'ssr' ? !isNodeBuiltin(id) : !id.startsWith('node:') } async originalResolveUrl(id: string, importer?: string) { @@ -142,6 +226,35 @@ export class VitestExecutor extends ViteNodeRunner { } } + protected async runModule(context: Record, transformed: string) { + const vmContext = this.options.context + + if (!vmContext || !this.externalModules) + return super.runModule(context, transformed) + + // add 'use strict' since ESM enables it by default + const codeDefinition = `'use strict';async (${Object.keys(context).join(',')})=>{{` + const code = `${codeDefinition}${transformed}\n}}` + const options = { + filename: context.__filename, + lineOffset: 0, + columnOffset: -codeDefinition.length, + } + + const fn = vm.runInContext(code, vmContext, { + ...options, + // if we encountered an import, it's not inlined + importModuleDynamically: this.externalModules.importModuleDynamically as any, + } as any) + await fn(...Object.values(context)) + } + + public async importExternalModule(path: string): Promise { + if (this.externalModules) + return this.externalModules.import(path) + return super.importExternalModule(path) + } + async dependencyRequest(id: string, fsPath: string, callstack: string[]): Promise { const mocked = await this.mocker.requestWithMock(fsPath, callstack) @@ -153,14 +266,16 @@ export class VitestExecutor extends ViteNodeRunner { } prepareContext(context: Record) { - const workerState = getWorkerState() - // support `import.meta.vitest` for test entry - if (workerState.filepath && normalize(workerState.filepath) === normalize(context.__filename)) { + if (this.state.filepath && normalize(this.state.filepath) === normalize(context.__filename)) { + const globalNamespace = this.options.context || globalThis // @ts-expect-error injected untyped global - Object.defineProperty(context.__vite_ssr_import_meta__, 'vitest', { get: () => globalThis.__vitest_index__ }) + Object.defineProperty(context.__vite_ssr_import_meta__, 'vitest', { get: () => globalNamespace.__vitest_index__ }) } + if (this.options.context && this.externalModules) + context.require = this.externalModules.createRequire(context.__filename) + return context } } diff --git a/packages/vitest/src/runtime/external-executor.ts b/packages/vitest/src/runtime/external-executor.ts new file mode 100644 index 000000000000..dcb903b7a15b --- /dev/null +++ b/packages/vitest/src/runtime/external-executor.ts @@ -0,0 +1,624 @@ +/* eslint-disable antfu/no-cjs-exports */ + +import vm from 'node:vm' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { dirname } from 'node:path' +import { Module as _Module, createRequire } from 'node:module' +import { readFileSync, statSync } from 'node:fs' +import { basename, extname, join, normalize } from 'pathe' +import { getCachedData, isNodeBuiltin, setCacheData } from 'vite-node/utils' +import { CSS_LANGS_RE, KNOWN_ASSET_RE } from 'vite-node/constants' +import { getColors } from '@vitest/utils' + +// need to copy paste types for vm +// because they require latest @types/node which we don't bundle + +interface ModuleEvaluateOptions { + timeout?: vm.RunningScriptOptions['timeout'] | undefined + breakOnSigint?: vm.RunningScriptOptions['breakOnSigint'] | undefined +} + +type ModuleLinker = (specifier: string, referencingModule: VMModule, extra: { assert: Object }) => VMModule | Promise +type ModuleStatus = 'unlinked' | 'linking' | 'linked' | 'evaluating' | 'evaluated' | 'errored' +declare class VMModule { + dependencySpecifiers: readonly string[] + error: any + identifier: string + context: vm.Context + namespace: Object + status: ModuleStatus + evaluate(options?: ModuleEvaluateOptions): Promise + link(linker: ModuleLinker): Promise +} +interface SyntheticModuleOptions { + /** + * String used in stack traces. + * @default 'vm:module(i)' where i is a context-specific ascending index. + */ + identifier?: string | undefined + /** + * The contextified object as returned by the `vm.createContext()` method, to compile and evaluate this module in. + */ + context?: vm.Context | undefined +} +declare class VMSyntheticModule extends VMModule { + /** + * Creates a new `SyntheticModule` instance. + * @param exportNames Array of names that will be exported from the module. + * @param evaluateCallback Called when the module is evaluated. + */ + constructor(exportNames: string[], evaluateCallback: (this: VMSyntheticModule) => void, options?: SyntheticModuleOptions) + /** + * This method is used after the module is linked to set the values of exports. + * If it is called before the module is linked, an `ERR_VM_MODULE_STATUS` error will be thrown. + * @param name + * @param value + */ + setExport(name: string, value: any): void +} + +declare interface ImportModuleDynamically { + (specifier: string, script: VMModule, importAssertions: Object): VMModule | Promise +} + +interface SourceTextModuleOptions { + identifier?: string | undefined + cachedData?: vm.ScriptOptions['cachedData'] | undefined + context?: vm.Context | undefined + lineOffset?: vm.BaseOptions['lineOffset'] | undefined + columnOffset?: vm.BaseOptions['columnOffset'] | undefined + /** + * Called during evaluation of this module to initialize the `import.meta`. + */ + initializeImportMeta?: ((meta: ImportMeta, module: VMSourceTextModule) => void) | undefined + importModuleDynamically?: ImportModuleDynamically +} +declare class VMSourceTextModule extends VMModule { + /** + * Creates a new `SourceTextModule` instance. + * @param code JavaScript Module code to parse + */ + constructor(code: string, options?: SourceTextModuleOptions) +} + +const SyntheticModule: typeof VMSyntheticModule = (vm as any).SyntheticModule +const SourceTextModule: typeof VMSourceTextModule = (vm as any).SourceTextModule + +interface PrivateNodeModule extends NodeModule { + _compile(code: string, filename: string): void +} + +const _require = createRequire(import.meta.url) + +const nativeResolve = import.meta.resolve! + +const dataURIRegex + = /^data:(?text\/javascript|application\/json|application\/wasm)(?:;(?charset=utf-8|base64))?,(?.*)$/ + +interface ExternalModulesExecutorOptions { + context: vm.Context + packageCache: Map +} + +// TODO: improve Node.js strict mode support in #2854 +export class ExternalModulesExecutor { + private requireCache: Record = Object.create(null) + private builtinCache: Record = Object.create(null) + private moduleCache = new Map>() + private extensions: Record unknown> = Object.create(null) + + private esmLinkMap = new WeakMap>() + private context: vm.Context + + private fsCache = new Map() + private fsBufferCache = new Map() + + private Module: typeof _Module + private primitives: { + Object: typeof Object + Array: typeof Array + Error: typeof Error + } + + constructor(private options: ExternalModulesExecutorOptions) { + this.context = options.context + + const primitives = vm.runInContext('({ Object, Array, Error })', this.context) as { + Object: typeof Object + Array: typeof Array + Error: typeof Error + } + this.primitives = primitives + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const executor = this + + // primitive implementation, some fields are not filled yet, like "paths" - #2854 + this.Module = class Module { + exports: any + isPreloading = false + require: NodeRequire + id: string + filename: string + loaded: boolean + parent: null | Module | undefined + children: Module[] = [] + path: string + paths: string[] = [] + + constructor(id: string, parent?: Module) { + this.exports = primitives.Object.create(Object.prototype) + this.require = Module.createRequire(id) + // in our case the path should always be resolved already + this.path = dirname(id) + this.id = id + this.filename = id + this.loaded = false + this.parent = parent + } + + _compile(code: string, filename: string) { + const cjsModule = Module.wrap(code) + const script = new vm.Script(cjsModule, { + filename, + importModuleDynamically: executor.importModuleDynamically, + } as any) + // @ts-expect-error mark script with current identifier + script.identifier = filename + const fn = script.runInContext(executor.context) + const __dirname = dirname(filename) + executor.requireCache[filename] = this + try { + fn(this.exports, this.require, this, filename, __dirname) + return this.exports + } + finally { + this.loaded = true + } + } + + // exposed for external use, Node.js does the opposite + static _load = (request: string, parent: Module | undefined, _isMain: boolean) => { + const require = Module.createRequire(parent?.filename ?? request) + return require(request) + } + + static wrap = (script: string) => { + return Module.wrapper[0] + script + Module.wrapper[1] + } + + static wrapper = new primitives.Array( + '(function (exports, require, module, __filename, __dirname) { ', + '\n});', + ) + + static builtinModules = _Module.builtinModules + static findSourceMap = _Module.findSourceMap + static SourceMap = _Module.SourceMap + static syncBuiltinESMExports = _Module.syncBuiltinESMExports + + static _cache = executor.moduleCache + static _extensions = executor.extensions + + static createRequire = (filename: string) => { + return executor.createRequire(filename) + } + + static runMain = () => { + throw new primitives.Error('[vitest] "runMain" is not implemented.') + } + + // @ts-expect-error not typed + static _resolveFilename = _Module._resolveFilename + // @ts-expect-error not typed + static _findPath = _Module._findPath + // @ts-expect-error not typed + static _initPaths = _Module._initPaths + // @ts-expect-error not typed + static _preloadModules = _Module._preloadModules + // @ts-expect-error not typed + static _resolveLookupPaths = _Module._resolveLookupPaths + // @ts-expect-error not typed + static globalPaths = _Module.globalPaths + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error + // @ts-ignore not typed in lower versions + static isBuiltin = _Module.isBuiltin + + static Module = Module + } + + this.extensions['.js'] = this.requireJs + this.extensions['.json'] = this.requireJson + } + + private requireJs = (m: NodeModule, filename: string) => { + const content = this.readFile(filename) + ;(m as PrivateNodeModule)._compile(content, filename) + } + + private requireJson = (m: NodeModule, filename: string) => { + const code = this.readFile(filename) + m.exports = JSON.parse(code) + } + + public importModuleDynamically = async (specifier: string, referencer: VMModule) => { + const module = await this.resolveModule(specifier, referencer.identifier) + return this.evaluateModule(module) + } + + private resolveModule = async (specifier: string, referencer: string) => { + const identifier = await this.resolveAsync(specifier, referencer) + return await this.createModule(identifier) + } + + private async resolveAsync(specifier: string, parent: string) { + return nativeResolve(specifier, parent) + } + + private readFile(path: string) { + const cached = this.fsCache.get(path) + if (cached) + return cached + const source = readFileSync(path, 'utf-8') + this.fsCache.set(path, source) + return source + } + + private readBuffer(path: string) { + const cached = this.fsBufferCache.get(path) + if (cached) + return cached + const buffer = readFileSync(path) + this.fsBufferCache.set(path, buffer) + return buffer + } + + private findNearestPackageData(basedir: string) { + const originalBasedir = basedir + const packageCache = this.options.packageCache + while (basedir) { + const cached = getCachedData(packageCache, basedir, originalBasedir) + if (cached) + return cached + + const pkgPath = join(basedir, 'package.json') + try { + if (statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) { + const pkgData = JSON.parse(this.readFile(pkgPath)) + + if (packageCache) + setCacheData(packageCache, pkgData, basedir, originalBasedir) + + return pkgData + } + } + catch {} + + const nextBasedir = dirname(basedir) + if (nextBasedir === basedir) + break + basedir = nextBasedir + } + + return null + } + + private wrapSynteticModule(identifier: string, exports: Record) { + // TODO: technically module should be parsed to find static exports, implement for strict mode in #2854 + const moduleKeys = Object.keys(exports).filter(key => key !== 'default') + const m: any = new SyntheticModule( + [...moduleKeys, 'default'], + () => { + for (const key of moduleKeys) + m.setExport(key, exports[key]) + m.setExport('default', exports) + }, + { + context: this.context, + identifier, + }, + ) + return m + } + + private async evaluateModule(m: T): Promise { + if (m.status === 'unlinked') { + this.esmLinkMap.set( + m, + m.link((identifier, referencer) => + this.resolveModule(identifier, referencer.identifier), + ), + ) + } + + await this.esmLinkMap.get(m) + + if (m.status === 'linked') + await m.evaluate() + + return m + } + + private findLongestRegisteredExtension(filename: string) { + const name = basename(filename) + let currentExtension: string + let index: number + let startIndex = 0 + // eslint-disable-next-line no-cond-assign + while ((index = name.indexOf('.', startIndex)) !== -1) { + startIndex = index + 1 + if (index === 0) + continue // Skip dotfiles like .gitignore + currentExtension = (name.slice(index)) + if (this.extensions[currentExtension]) + return currentExtension + } + return '.js' + } + + public createRequire = (filename: string) => { + const _require = createRequire(filename) + const require = ((id: string) => { + const resolved = _require.resolve(id) + const ext = extname(resolved) + if (ext === '.node' || isNodeBuiltin(resolved)) + return this.requireCoreModule(resolved) + const module = this.createCommonJSNodeModule(resolved) + return this.loadCommonJSModule(module, resolved) + }) as NodeRequire + require.resolve = _require.resolve + Object.defineProperty(require, 'extensions', { + get: () => this.extensions, + set: () => {}, + configurable: true, + }) + require.main = _require.main + require.cache = this.requireCache + return require + } + + private createCommonJSNodeModule(filename: string) { + return new this.Module(filename) + } + + // very naive implementation for Node.js require + private loadCommonJSModule(module: NodeModule, filename: string): Record { + const cached = this.requireCache[filename] + if (cached) + return cached.exports + + const extension = this.findLongestRegisteredExtension(filename) + const loader = this.extensions[extension] || this.extensions['.js'] + loader(module, filename) + + return module.exports + } + + private async createEsmModule(fileUrl: string, code: string) { + const cached = this.moduleCache.get(fileUrl) + if (cached) + return cached + const [urlPath] = fileUrl.split('?') + if (CSS_LANGS_RE.test(urlPath) || KNOWN_ASSET_RE.test(urlPath)) { + const path = normalize(urlPath) + let name = path.split('/node_modules/').pop() || '' + if (name?.startsWith('@')) + name = name.split('/').slice(0, 2).join('/') + else + name = name.split('/')[0] + const ext = extname(path) + let error = `[vitest] Cannot import ${fileUrl}. At the moment, importing ${ext} files inside external dependencies is not allowed. ` + if (name) { + const c = getColors() + error += 'As a temporary workaround you can try to inline the package by updating your config:' ++ `\n\n${ +c.gray(c.dim('// vitest.config.js')) +}\n${ +c.green(`export default { + test: { + deps: { + optimizer: { + web: { + include: [ + ${c.yellow(c.bold(`"${name}"`))} + ] + } + } + } + } +}\n`)}` + } + throw new this.primitives.Error(error) + } + // TODO: should not be allowed in strict mode, implement in #2854 + if (fileUrl.endsWith('.json')) { + const m = new SyntheticModule( + ['default'], + () => { + const result = JSON.parse(code) + m.setExport('default', result) + }, + ) + this.moduleCache.set(fileUrl, m) + return m + } + const m = new SourceTextModule( + code, + { + identifier: fileUrl, + context: this.context, + importModuleDynamically: this.importModuleDynamically, + initializeImportMeta: (meta, mod) => { + meta.url = mod.identifier + meta.resolve = (specifier: string, importer?: string) => { + return nativeResolve(specifier, importer ?? mod.identifier) + } + }, + }, + ) + this.moduleCache.set(fileUrl, m) + return m + } + + private requireCoreModule(identifier: string) { + const normalized = identifier.replace(/^node:/, '') + if (this.builtinCache[normalized]) + return this.builtinCache[normalized].exports + const moduleExports = _require(identifier) + if (identifier === 'node:module' || identifier === 'module') { + const module = new this.Module('/module.js') // path should not matter + module.exports = this.Module + this.builtinCache[normalized] = module + return module.exports + } + this.builtinCache[normalized] = _require.cache[normalized]! + return moduleExports + } + + private async loadWebAssemblyModule(source: Buffer, identifier: string) { + const cached = this.moduleCache.get(identifier) + if (cached) + return cached + + const wasmModule = await WebAssembly.compile(source) + + const exports = WebAssembly.Module.exports(wasmModule) + const imports = WebAssembly.Module.imports(wasmModule) + + const moduleLookup: Record = {} + for (const { module } of imports) { + if (moduleLookup[module] === undefined) { + const resolvedModule = await this.resolveModule( + module, + identifier, + ) + + moduleLookup[module] = await this.evaluateModule(resolvedModule) + } + } + + const syntheticModule = new SyntheticModule( + exports.map(({ name }) => name), + () => { + const importsObject: WebAssembly.Imports = {} + for (const { module, name } of imports) { + if (!importsObject[module]) + importsObject[module] = {} + + importsObject[module][name] = (moduleLookup[module].namespace as any)[name] + } + const wasmInstance = new WebAssembly.Instance( + wasmModule, + importsObject, + ) + for (const { name } of exports) + syntheticModule.setExport(name, wasmInstance.exports[name]) + }, + { context: this.context, identifier }, + ) + + return syntheticModule + } + + private async createDataModule(identifier: string): Promise { + const cached = this.moduleCache.get(identifier) + if (cached) + return cached + + const Error = this.primitives.Error + const match = identifier.match(dataURIRegex) + + if (!match || !match.groups) + throw new Error('Invalid data URI') + + const mime = match.groups.mime + const encoding = match.groups.encoding + + if (mime === 'application/wasm') { + if (!encoding) + throw new Error('Missing data URI encoding') + + if (encoding !== 'base64') + throw new Error(`Invalid data URI encoding: ${encoding}`) + + const module = await this.loadWebAssemblyModule( + Buffer.from(match.groups.code, 'base64'), + identifier, + ) + this.moduleCache.set(identifier, module) + return module + } + + let code = match.groups.code + if (!encoding || encoding === 'charset=utf-8') + code = decodeURIComponent(code) + + else if (encoding === 'base64') + code = Buffer.from(code, 'base64').toString() + else + throw new Error(`Invalid data URI encoding: ${encoding}`) + + if (mime === 'application/json') { + const module = new SyntheticModule( + ['default'], + () => { + const obj = JSON.parse(code) + module.setExport('default', obj) + }, + { context: this.context, identifier }, + ) + this.moduleCache.set(identifier, module) + return module + } + + return this.createEsmModule(identifier, code) + } + + private async createModule(identifier: string): Promise { + if (identifier.startsWith('data:')) + return this.createDataModule(identifier) + + const extension = extname(identifier) + + if (extension === '.node' || isNodeBuiltin(identifier)) { + const exports = this.requireCoreModule(identifier) + return this.wrapSynteticModule(identifier, exports) + } + + const isFileUrl = identifier.startsWith('file://') + const fileUrl = isFileUrl ? identifier : pathToFileURL(identifier).toString() + const pathUrl = isFileUrl ? fileURLToPath(identifier.split('?')[0]) : identifier + + // TODO: support wasm in the future + // if (extension === '.wasm') { + // const source = this.readBuffer(pathUrl) + // const wasm = this.loadWebAssemblyModule(source, fileUrl) + // this.moduleCache.set(fileUrl, wasm) + // return wasm + // } + + if (extension === '.cjs') { + const module = this.createCommonJSNodeModule(pathUrl) + const exports = this.loadCommonJSModule(module, pathUrl) + return this.wrapSynteticModule(fileUrl, exports) + } + + if (extension === '.mjs') + return await this.createEsmModule(fileUrl, this.readFile(pathUrl)) + + const pkgData = this.findNearestPackageData(normalize(pathUrl)) + + if (pkgData.type === 'module') + return await this.createEsmModule(fileUrl, this.readFile(pathUrl)) + + const module = this.createCommonJSNodeModule(pathUrl) + const exports = this.loadCommonJSModule(module, pathUrl) + return this.wrapSynteticModule(fileUrl, exports) + } + + async import(identifier: string) { + const module = await this.createModule(identifier) + await this.evaluateModule(module) + return module.namespace + } +} diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 4ac7f98a736a..adfd9485e981 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -1,14 +1,14 @@ import { existsSync, readdirSync } from 'node:fs' +import vm from 'node:vm' import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' import { getColors, getType } from '@vitest/utils' import { isNodeBuiltin } from 'vite-node/utils' -import { getWorkerState } from '../utils/global' +import { distDir } from '../paths' import { getAllMockableProperties } from '../utils/base' import type { MockFactory, PendingSuiteMock } from '../types/mocker' -import { spyOn } from '../integrations/spy' import type { VitestExecutor } from './execute' -const filterPublicKeys = ['__esModule', Symbol.asyncIterator, Symbol.hasInstance, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.matchAll, Symbol.replace, Symbol.search, Symbol.split, Symbol.species, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables] +const spyModulePath = resolve(distDir, 'spy.js') class RefTracker { private idMap = new Map() @@ -39,12 +39,34 @@ function isSpecialProp(prop: Key, parentType: string) { } export class VitestMocker { - public static pendingIds: PendingSuiteMock[] = [] + static pendingIds: PendingSuiteMock[] = [] + private spyModule?: typeof import('@vitest/spy') private resolveCache = new Map>() + private primitives: { + Object: typeof Object + Function: typeof Function + RegExp: typeof RegExp + Array: typeof Array + Map: typeof Map + Error: typeof Error + Symbol: typeof Symbol + } + + private filterPublicKeys: (symbol | string)[] constructor( public executor: VitestExecutor, - ) {} + ) { + const context = this.executor.options.context + if (context) + this.primitives = vm.runInContext('({ Object, Error, Function, RegExp, Symbol, Array, Map })', context) + else + this.primitives = { Object, Error, Function, RegExp, Symbol: globalThis.Symbol, Array, Map } + + const Symbol = this.primitives.Symbol + + this.filterPublicKeys = ['__esModule', Symbol.asyncIterator, Symbol.hasInstance, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.matchAll, Symbol.replace, Symbol.search, Symbol.split, Symbol.species, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables] + } private get root() { return this.executor.options.root @@ -62,6 +84,10 @@ export class VitestMocker { return this.executor.options.moduleDirectories || [] } + public async initializeSpyModule() { + this.spyModule = await this.executor.executeId(spyModulePath) + } + private deleteCachedItem(id: string) { const mockId = this.getMockPath(id) if (this.moduleCache.has(mockId)) @@ -73,7 +99,12 @@ export class VitestMocker { } public getSuiteFilepath(): string { - return getWorkerState().filepath || 'global' + return this.executor.state.filepath || 'global' + } + + private createError(message: string) { + const Error = this.primitives.Error + return new Error(message) } public getMocks() { @@ -139,7 +170,7 @@ export class VitestMocker { exports = await mock() } catch (err) { - const vitestError = new Error( + const vitestError = this.createError( '[vitest] There was an error when mocking a module. ' + 'If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. ' + 'Read more: https://vitest.dev/api/vi.html#vi-mock') @@ -151,10 +182,10 @@ export class VitestMocker { const mockpath = this.resolveCache.get(this.getSuiteFilepath())?.[filepath] || filepath if (exports === null || typeof exports !== 'object') - throw new Error(`[vitest] vi.mock("${mockpath}", factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?`) + throw this.createError(`[vitest] vi.mock("${mockpath}", factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?`) const moduleExports = new Proxy(exports, { - get(target, prop) { + get: (target, prop) => { const val = target[prop] // 'then' can exist on non-Promise objects, need nested instanceof check for logic to work @@ -163,10 +194,10 @@ export class VitestMocker { return target.then.bind(target) } else if (!(prop in target)) { - if (filterPublicKeys.includes(prop)) + if (this.filterPublicKeys.includes(prop)) return undefined const c = getColors() - throw new Error( + throw this.createError( `[vitest] No "${String(prop)}" export is defined on the "${mockpath}" mock. ` + 'Did you forget to return it from "vi.mock"?' + '\nIf you need to partially mock a module, you can use "vi.importActual" inside:\n\n' @@ -248,7 +279,7 @@ export class VitestMocker { const mockPropertiesOf = (container: Record, newContainer: Record) => { const containerType = getType(container) const isModule = containerType === 'Module' || !!container.__esModule - for (const { key: property, descriptor } of getAllMockableProperties(container, isModule)) { + for (const { key: property, descriptor } of getAllMockableProperties(container, isModule, this.primitives)) { // Modules define their exports as getters. We want to process those. if (!isModule && descriptor.get) { try { @@ -293,7 +324,10 @@ export class VitestMocker { continue if (isFunction) { - const mock = spyOn(newContainer, property).mockImplementation(() => undefined) + const spyModule = this.spyModule + if (!spyModule) + throw this.createError('[vitest] `spyModule` is not defined. This is Vitest error. Please open a new issue with reproduction.') + const mock = spyModule.spyOn(newContainer, property).mockImplementation(() => undefined) mock.mockRestore = () => { mock.mockReset() mock.mockImplementation(() => undefined) diff --git a/packages/vitest/src/runtime/rpc.ts b/packages/vitest/src/runtime/rpc.ts index 2e50bbb9be64..0357c059f5a1 100644 --- a/packages/vitest/src/runtime/rpc.ts +++ b/packages/vitest/src/runtime/rpc.ts @@ -4,6 +4,7 @@ import { import type { BirpcReturn } from 'birpc' import { getWorkerState } from '../utils/global' import type { RuntimeRPC } from '../types/rpc' +import type { WorkerRPC } from '../types' const { get } = Reflect @@ -52,8 +53,7 @@ export async function rpcDone() { return Promise.all(awaitable) } -export function rpc(): BirpcReturn { - const { rpc } = getWorkerState() +export function createSafeRpc(rpc: WorkerRPC) { return new Proxy(rpc, { get(target, p, handler) { const sendCall = get(target, p, handler) @@ -72,3 +72,8 @@ export function rpc(): BirpcReturn { }, }) } + +export function rpc(): BirpcReturn { + const { rpc } = getWorkerState() + return rpc +} diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts new file mode 100644 index 000000000000..761c431147e7 --- /dev/null +++ b/packages/vitest/src/runtime/runners/index.ts @@ -0,0 +1,84 @@ +import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner' +import { resolve } from 'pathe' +import type { ResolvedConfig } from '../../types/config' +import type { VitestExecutor } from '../execute' +import { distDir } from '../../paths' +import { getWorkerState } from '../../utils/global' +import { rpc } from '../rpc' +import { takeCoverageInsideWorker } from '../../integrations/coverage' + +const runnersFile = resolve(distDir, 'runners.js') + +async function getTestRunnerConstructor(config: ResolvedConfig, executor: VitestExecutor): Promise { + if (!config.runner) { + const { VitestTestRunner, NodeBenchmarkRunner } = await executor.executeFile(runnersFile) + return (config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner) as VitestRunnerConstructor + } + const mod = await executor.executeId(config.runner) + if (!mod.default && typeof mod.default !== 'function') + throw new Error(`Runner must export a default function, but got ${typeof mod.default} imported from ${config.runner}`) + return mod.default as VitestRunnerConstructor +} + +export async function resolveTestRunner(config: ResolvedConfig, executor: VitestExecutor): Promise { + const TestRunner = await getTestRunnerConstructor(config, executor) + const testRunner = new TestRunner(config) + + // inject private executor to every runner + Object.defineProperty(testRunner, '__vitest_executor', { + value: executor, + enumerable: false, + configurable: false, + }) + + if (!testRunner.config) + testRunner.config = config + + if (!testRunner.importFile) + throw new Error('Runner must implement "importFile" method.') + + // patch some methods, so custom runners don't need to call RPC + const originalOnTaskUpdate = testRunner.onTaskUpdate + testRunner.onTaskUpdate = async (task) => { + const p = rpc().onTaskUpdate(task) + await originalOnTaskUpdate?.call(testRunner, task) + return p + } + + const originalOnCollected = testRunner.onCollected + testRunner.onCollected = async (files) => { + const state = getWorkerState() + files.forEach((file) => { + file.prepareDuration = state.durations.prepare + file.environmentLoad = state.durations.environment + // should be collected only for a single test file in a batch + state.durations.prepare = 0 + state.durations.environment = 0 + }) + rpc().onCollected(files) + await originalOnCollected?.call(testRunner, files) + } + + const originalOnAfterRun = testRunner.onAfterRun + testRunner.onAfterRun = async (files) => { + const coverage = await takeCoverageInsideWorker(config.coverage, executor) + rpc().onAfterSuiteRun({ coverage }) + await originalOnAfterRun?.call(testRunner, files) + } + + const originalOnAfterRunTest = testRunner.onAfterRunTest + testRunner.onAfterRunTest = async (test) => { + if (config.bail && test.result?.state === 'fail') { + const previousFailures = await rpc().getCountOfFailedTests() + const currentFailures = 1 + previousFailures + + if (currentFailures >= config.bail) { + rpc().onCancel('test-failure') + testRunner.onCancel?.('test-failure') + } + } + await originalOnAfterRunTest?.call(testRunner, test) + } + + return testRunner +} diff --git a/packages/vitest/src/runtime/setup.node.ts b/packages/vitest/src/runtime/setup.node.ts index 15625fcad1f7..2e1a1aa3ee89 100644 --- a/packages/vitest/src/runtime/setup.node.ts +++ b/packages/vitest/src/runtime/setup.node.ts @@ -2,18 +2,16 @@ import { createRequire } from 'node:module' import { isatty } from 'node:tty' import { installSourcemapsSupport } from 'vite-node/source-map' import { createColors, setupColors } from '@vitest/utils' -import type { EnvironmentOptions, ResolvedConfig, ResolvedTestEnvironment } from '../types' +import type { EnvironmentOptions, ResolvedConfig, ResolvedTestEnvironment, WorkerGlobalState } from '../types' import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node' import { getSafeTimers, getWorkerState } from '../utils' import * as VitestIndex from '../index' -import { RealDate } from '../integrations/mock/date' import { expect } from '../integrations/chai' -import { rpc } from './rpc' import { setupCommonEnv } from './setup.common' // this should only be used in Node let globalSetup = false -export async function setupGlobalEnv(config: ResolvedConfig) { +export async function setupGlobalEnv(config: ResolvedConfig, { environment }: ResolvedTestEnvironment) { await setupCommonEnv(config) Object.defineProperty(globalThis, '__vitest_index__', { @@ -24,7 +22,7 @@ export async function setupGlobalEnv(config: ResolvedConfig) { const state = getWorkerState() if (!state.config.snapshotOptions.snapshotEnvironment) - state.config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment() + state.config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(state.rpc) if (globalSetup) return @@ -32,138 +30,37 @@ export async function setupGlobalEnv(config: ResolvedConfig) { globalSetup = true setupColors(createColors(isatty(1))) - const _require = createRequire(import.meta.url) - // always mock "required" `css` files, because we cannot process them - _require.extensions['.css'] = () => ({}) - _require.extensions['.scss'] = () => ({}) - _require.extensions['.sass'] = () => ({}) - _require.extensions['.less'] = () => ({}) + if (environment.transformMode === 'web') { + const _require = createRequire(import.meta.url) + // always mock "required" `css` files, because we cannot process them + _require.extensions['.css'] = () => ({}) + _require.extensions['.scss'] = () => ({}) + _require.extensions['.sass'] = () => ({}) + _require.extensions['.less'] = () => ({}) + } installSourcemapsSupport({ getSourceMap: source => state.moduleCache.getSourceMap(source), }) - await setupConsoleLogSpy() + await setupConsoleLogSpy(state) } -export async function setupConsoleLogSpy() { - const stdoutBuffer = new Map() - const stderrBuffer = new Map() - const timers = new Map() - const unknownTestId = '__vitest__unknown_test__' - - const { Writable } = await import('node:stream') - const { Console } = await import('node:console') - const { setTimeout, clearTimeout } = getSafeTimers() +export async function setupConsoleLogSpy(state: WorkerGlobalState) { + const { createCustomConsole } = await import('./console') - // group sync console.log calls with macro task - function schedule(taskId: string) { - const timer = timers.get(taskId)! - const { stdoutTime, stderrTime } = timer - clearTimeout(timer.timer) - timer.timer = setTimeout(() => { - if (stderrTime < stdoutTime) { - sendStderr(taskId) - sendStdout(taskId) - } - else { - sendStdout(taskId) - sendStderr(taskId) - } - }) - } - function sendStdout(taskId: string) { - const buffer = stdoutBuffer.get(taskId) - if (!buffer) - return - const content = buffer.map(i => String(i)).join('') - const timer = timers.get(taskId)! - rpc().onUserConsoleLog({ - type: 'stdout', - content: content || '', - taskId, - time: timer.stdoutTime || RealDate.now(), - size: buffer.length, - }) - stdoutBuffer.set(taskId, []) - timer.stdoutTime = 0 - } - function sendStderr(taskId: string) { - const buffer = stderrBuffer.get(taskId) - if (!buffer) - return - const content = buffer.map(i => String(i)).join('') - const timer = timers.get(taskId)! - rpc().onUserConsoleLog({ - type: 'stderr', - content: content || '', - taskId, - time: timer.stderrTime || RealDate.now(), - size: buffer.length, - }) - stderrBuffer.set(taskId, []) - timer.stderrTime = 0 - } - - const stdout = new Writable({ - write(data, encoding, callback) { - const id = getWorkerState()?.current?.id ?? unknownTestId - let timer = timers.get(id) - if (timer) { - timer.stdoutTime = timer.stdoutTime || RealDate.now() - } - else { - timer = { stdoutTime: RealDate.now(), stderrTime: RealDate.now(), timer: 0 } - timers.set(id, timer) - } - let buffer = stdoutBuffer.get(id) - if (!buffer) { - buffer = [] - stdoutBuffer.set(id, buffer) - } - buffer.push(data) - schedule(id) - callback() - }, - }) - const stderr = new Writable({ - write(data, encoding, callback) { - const id = getWorkerState()?.current?.id ?? unknownTestId - let timer = timers.get(id) - if (timer) { - timer.stderrTime = timer.stderrTime || RealDate.now() - } - else { - timer = { stderrTime: RealDate.now(), stdoutTime: RealDate.now(), timer: 0 } - timers.set(id, timer) - } - let buffer = stderrBuffer.get(id) - if (!buffer) { - buffer = [] - stderrBuffer.set(id, buffer) - } - buffer.push(data) - schedule(id) - callback() - }, - }) - globalThis.console = new Console({ - stdout, - stderr, - colorMode: true, - groupIndentation: 2, - }) + globalThis.console = createCustomConsole(state) } export async function withEnv( - { environment, name }: ResolvedTestEnvironment, + { environment }: ResolvedTestEnvironment, options: EnvironmentOptions, fn: () => Promise, ) { // @ts-expect-error untyped global - globalThis.__vitest_environment__ = name + globalThis.__vitest_environment__ = environment.name expect.setState({ - environment: name, + environment: environment.name, }) const env = await environment.setup(globalThis, options) try { diff --git a/packages/vitest/src/runtime/vm.ts b/packages/vitest/src/runtime/vm.ts new file mode 100644 index 000000000000..6c016c1c5919 --- /dev/null +++ b/packages/vitest/src/runtime/vm.ts @@ -0,0 +1,119 @@ +import { pathToFileURL } from 'node:url' +import { performance } from 'node:perf_hooks' +import { isContext } from 'node:vm' +import { ModuleCacheMap } from 'vite-node/client' +import { workerId as poolId } from 'tinypool' +import { createBirpc } from 'birpc' +import { resolve } from 'pathe' +import { installSourcemapsSupport } from 'vite-node/source-map' +import type { CancelReason } from '@vitest/runner' +import type { RuntimeRPC, WorkerContext, WorkerGlobalState } from '../types' +import { distDir } from '../paths' +import { loadEnvironment } from '../integrations/env' +import { startVitestExecutor } from './execute' +import { createCustomConsole } from './console' +import { createSafeRpc } from './rpc' + +const entryFile = pathToFileURL(resolve(distDir, 'entry-vm.js')).href + +export async function run(ctx: WorkerContext) { + const moduleCache = new ModuleCacheMap() + const mockMap = new Map() + const { config, port } = ctx + + let setCancel = (_reason: CancelReason) => {} + const onCancel = new Promise((resolve) => { + setCancel = resolve + }) + + const rpc = createBirpc( + { + onCancel: setCancel, + }, + { + eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onWorkerExit'], + post(v) { port.postMessage(v) }, + on(fn) { port.addListener('message', fn) }, + }, + ) + + const environment = await loadEnvironment(ctx.environment.name, ctx.config.root) + + if (!environment.setupVM) { + const envName = ctx.environment.name + const packageId = envName[0] === '.' ? envName : `vitest-environment-${envName}` + throw new TypeError( + `Environment "${ctx.environment.name}" is not a valid environment. ` + + `Path "${packageId}" doesn't support vm environment because it doesn't provide "setupVM" method.`, + ) + } + + const state: WorkerGlobalState = { + ctx, + moduleCache, + config, + mockMap, + onCancel, + environment, + durations: { + environment: performance.now(), + prepare: performance.now(), + }, + rpc: createSafeRpc(rpc), + } + + installSourcemapsSupport({ + getSourceMap: source => moduleCache.getSourceMap(source), + }) + + const vm = await environment.setupVM(ctx.environment.options || ctx.config.environmentOptions || {}) + + state.durations.environment = performance.now() - state.durations.environment + + process.env.VITEST_WORKER_ID = String(ctx.workerId) + process.env.VITEST_POOL_ID = String(poolId) + process.env.VITEST_VM_POOL = '1' + + if (!vm.getVmContext) + throw new TypeError(`Environment ${ctx.environment.name} doesn't provide "getVmContext" method. It should return a context created by "vm.createContext" method.`) + + const context = vm.getVmContext() + + if (!isContext(context)) + throw new TypeError(`Environment ${ctx.environment.name} doesn't provide a valid context. It should be created by "vm.createContext" method.`) + + context.__vitest_worker__ = state + // this is unfortunately needed for our own dependencies + // we need to find a way to not rely on this by default + // because browser doesn't provide these globals + context.process = process + context.global = context + context.console = createCustomConsole(state) + + if (ctx.invalidates) { + ctx.invalidates.forEach((fsPath) => { + moduleCache.delete(fsPath) + moduleCache.delete(`mock:${fsPath}`) + }) + } + ctx.files.forEach(i => moduleCache.delete(i)) + + const executor = await startVitestExecutor({ + context, + moduleCache, + mockMap, + state, + }) + + context.__vitest_mocker__ = executor.mocker + + const { run } = await executor.importExternalModule(entryFile) + + try { + await run(ctx.files, ctx.config, executor) + } + finally { + await vm.teardown?.() + state.environmentTeardownRun = true + } +} diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index ce2fa1a8b5f4..7bb1118c6d4c 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -2,13 +2,14 @@ import { performance } from 'node:perf_hooks' import { createBirpc } from 'birpc' import { workerId as poolId } from 'tinypool' import type { CancelReason } from '@vitest/runner' -import type { RunnerRPC, RuntimeRPC, WorkerContext } from '../types' +import type { RunnerRPC, RuntimeRPC, WorkerContext, WorkerGlobalState } from '../types' import { getWorkerState } from '../utils/global' +import { loadEnvironment } from '../integrations/env' import { mockMap, moduleCache, startViteNode } from './execute' import { setupInspect } from './inspector' -import { rpcDone } from './rpc' +import { createSafeRpc, rpcDone } from './rpc' -function init(ctx: WorkerContext) { +async function init(ctx: WorkerContext) { // @ts-expect-error untyped global if (typeof __vitest_worker__ !== 'undefined' && ctx.config.threads && ctx.config.isolate) throw new Error(`worker for ${ctx.files.join(',')} already initialized by ${getWorkerState().ctx.files.join(',')}. This is probably an internal bug of Vitest.`) @@ -23,31 +24,38 @@ function init(ctx: WorkerContext) { setCancel = resolve }) - // @ts-expect-error untyped global - globalThis.__vitest_environment__ = config.environment.name - // @ts-expect-error I know what I am doing :P - globalThis.__vitest_worker__ = { + const rpc = createBirpc( + { + onCancel: setCancel, + }, + { + eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onWorkerExit', 'onCancel'], + post(v) { port.postMessage(v) }, + on(fn) { port.addListener('message', fn) }, + }, + ) + + const environment = await loadEnvironment(ctx.environment.name, ctx.config.root) + if (ctx.environment.transformMode) + environment.transformMode = ctx.environment.transformMode + + const state: WorkerGlobalState = { ctx, moduleCache, config, mockMap, onCancel, + environment, durations: { environment: 0, prepare: performance.now(), }, - rpc: createBirpc( - { - onCancel: setCancel, - }, - { - eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onWorkerExit', 'onCancel'], - post(v) { port.postMessage(v) }, - on(fn) { port.addListener('message', fn) }, - }, - ), + rpc: createSafeRpc(rpc), } + // @ts-expect-error I know what I am doing :P + globalThis.__vitest_worker__ = state + if (ctx.invalidates) { ctx.invalidates.forEach((fsPath) => { moduleCache.delete(fsPath) @@ -55,15 +63,17 @@ function init(ctx: WorkerContext) { }) } ctx.files.forEach(i => moduleCache.delete(i)) + + return state } export async function run(ctx: WorkerContext) { const inspectorCleanup = setupInspect(ctx.config) try { - init(ctx) - const { run, executor, environment } = await startViteNode(ctx) - await run(ctx.files, ctx.config, { ...ctx.environment, environment }, executor) + const state = await init(ctx) + const { run, executor } = await startViteNode({ state }) + await run(ctx.files, ctx.config, { environment: state.environment, options: ctx.environment.options }, executor) await rpcDone() } finally { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 92e20d67f886..5b0b80bd19d6 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -5,7 +5,7 @@ import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' import type { ViteNodeServerOptions } from 'vite-node' import type { BuiltinReporters } from '../node/reporters' import type { TestSequencerConstructor } from '../node/sequencers/types' -import type { ChaiConfig } from '../integrations/chai' +import type { ChaiConfig } from '../integrations/chai/config' import type { CoverageOptions, ResolvedCoverageOptions } from './coverage' import type { JSDOMOptions } from './jsdom-options' import type { Reporter } from './reporter' @@ -19,7 +19,7 @@ export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' // Record is used, so user can get intellisense for builtin environments, but still allow custom environments export type VitestEnvironment = BuiltinEnvironment | (string & Record) -export type VitestPool = 'browser' | 'threads' | 'child_process' +export type VitestPool = 'browser' | 'threads' | 'child_process' | 'experimentalVmThreads' export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped' export type ApiConfig = Pick @@ -301,6 +301,21 @@ export interface InlineConfig { */ outputFile?: string | (Partial> & Record) + /** + * Run tests using VM context in a worker pool. + * + * This makes tests run faster, but VM module is unstable. Your tests might leak memory. + */ + experimentalVmThreads?: boolean + + /** + * Specifies the memory limit for workers before they are recycled. + * If you see your worker leaking memory, try to tinker this value. + * + * This only has effect on workers that run tests in VM context. + */ + experimentalVmWorkerMemoryLimit?: string | number + /** * Enable multi-threading * @@ -693,7 +708,7 @@ export interface UserConfig extends InlineConfig { shard?: string } -export interface ResolvedConfig extends Omit, 'config' | 'filters' | 'browser' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters' | 'resolveSnapshotPath' | 'benchmark' | 'shard' | 'cache' | 'sequence' | 'typecheck' | 'runner'> { +export interface ResolvedConfig extends Omit, 'config' | 'filters' | 'browser' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters' | 'resolveSnapshotPath' | 'benchmark' | 'shard' | 'cache' | 'sequence' | 'typecheck' | 'runner' | 'experimentalVmWorkerMemoryLimit'> { mode: VitestRunMode base?: string @@ -738,6 +753,8 @@ export interface ResolvedConfig extends Omit, 'config' | 'f typecheck: TypecheckConfig runner?: string + + experimentalVmWorkerMemoryLimit?: number | null } export type ProjectConfig = Omit< diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 7e8da33bdbcd..776ac609adeb 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -18,12 +18,18 @@ export interface ModuleCache { } export interface EnvironmentReturn { - teardown: (global: any) => Awaitable + teardown(global: any): Awaitable +} + +export interface VmEnvironmentReturn { + getVmContext(): { [key: string]: any } + teardown(): Awaitable } export interface Environment { name: string - transformMode?: 'web' | 'ssr' + transformMode: 'web' | 'ssr' + setupVM?(options: Record): Awaitable setup(global: any, options: Record): Awaitable } diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts index 0111737947a3..e0d92a8ed0a3 100644 --- a/packages/vitest/src/types/index.ts +++ b/packages/vitest/src/types/index.ts @@ -3,16 +3,16 @@ import './global' export { expectTypeOf, type ExpectTypeOf } from '../typecheck/expectTypeOf' export { assertType, type AssertType } from '../typecheck/assertType' -export * from '../typecheck/types' -export * from './config' -export * from './tasks' -export * from './rpc' -export * from './reporter' -export * from './snapshot' -export * from './worker' -export * from './general' -export * from './coverage' -export * from './benchmark' +export type * from '../typecheck/types' +export type * from './config' +export type * from './tasks' +export type * from './rpc' +export type * from './reporter' +export type * from './snapshot' +export type * from './worker' +export type * from './general' +export type * from './coverage' +export type * from './benchmark' export type { EnhancedSpy, MockedFunction, diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index 6f875b088be9..9d0721b8864b 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -21,8 +21,8 @@ export interface RuntimeRPC { onCollected: (files: File[]) => void onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void onTaskUpdate: (pack: TaskResultPack[]) => void - onCancel(reason: CancelReason): void - getCountOfFailedTests(): number + onCancel: (reason: CancelReason) => void + getCountOfFailedTests: () => number snapshotSaved: (snapshot: SnapshotResult) => void resolveSnapshotPath: (testPath: string) => string @@ -39,8 +39,7 @@ export interface ContextTestEnvironment { options: EnvironmentOptions | null } -export interface ResolvedTestEnvironment extends ContextTestEnvironment { - name: VitestEnvironment +export interface ResolvedTestEnvironment { environment: Environment options: EnvironmentOptions | null } diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index b6224e8174fe..7d3c4cde3a6f 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -5,6 +5,7 @@ import type { BirpcReturn } from 'birpc' import type { MockMap } from './mocker' import type { ResolvedConfig } from './config' import type { ContextRPC, RuntimeRPC } from './rpc' +import type { Environment } from './general' export interface WorkerContext extends ContextRPC { workerId: number @@ -17,12 +18,15 @@ export interface AfterSuiteRunMeta { coverage?: unknown } +export type WorkerRPC = BirpcReturn + export interface WorkerGlobalState { - ctx: WorkerContext + ctx: ContextRPC config: ResolvedConfig - rpc: BirpcReturn + rpc: WorkerRPC current?: Test filepath?: string + environment: Environment environmentTeardownRun?: boolean onCancel: Promise moduleCache: ModuleCacheMap diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index 010a1b0e3107..151de2a8cdf8 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -2,8 +2,12 @@ import type { Arrayable, Nullable } from '../types' export { notNullish, getCallLastIndex } from '@vitest/utils' -function isFinalObj(obj: any) { - return obj === Object.prototype || obj === Function.prototype || obj === RegExp.prototype +export interface GlobalConstructors { + Object: ObjectConstructor + Function: FunctionConstructor + RegExp: RegExpConstructor + Array: ArrayConstructor + Map: MapConstructor } function collectOwnProperties(obj: any, collector: Set | ((key: string | symbol) => void)) { @@ -25,12 +29,20 @@ export function isPrimitive(value: unknown) { return value === null || (typeof value !== 'function' && typeof value !== 'object') } -export function getAllMockableProperties(obj: any, isModule: boolean) { +export function getAllMockableProperties(obj: any, isModule: boolean, constructors: GlobalConstructors) { + const { + Map, + Object, + Function, + RegExp, + Array, + } = constructors + const allProps = new Map() let curr = obj do { // we don't need properties from these - if (isFinalObj(curr)) + if (curr === Object.prototype || curr === Function.prototype || curr === RegExp.prototype) break collectOwnProperties(curr, (key) => { diff --git a/packages/vitest/src/utils/global.ts b/packages/vitest/src/utils/global.ts index 02db76c687b0..ea7398b6eb1e 100644 --- a/packages/vitest/src/utils/global.ts +++ b/packages/vitest/src/utils/global.ts @@ -6,6 +6,6 @@ export function getWorkerState(): WorkerGlobalState { } export function getCurrentEnvironment(): string { - // @ts-expect-error untyped global - return globalThis.__vitest_environment__ + const state = getWorkerState() + return state?.environment.name } diff --git a/packages/vitest/src/utils/index.ts b/packages/vitest/src/utils/index.ts index 7cf3ea9540bf..5464456d13b3 100644 --- a/packages/vitest/src/utils/index.ts +++ b/packages/vitest/src/utils/index.ts @@ -1,5 +1,4 @@ import { relative } from 'pathe' -import type { ModuleCacheMap } from 'vite-node' import { getWorkerState } from '../utils' import { isNode } from './env' @@ -8,8 +7,8 @@ export * from './tasks' export * from './base' export * from './global' export * from './timers' -export * from './import' export * from './env' +export * from './modules' export const isWindows = isNode && process.platform === 'win32' export function getRunMode() { @@ -25,25 +24,6 @@ export function isRunningInBenchmark() { export const relativePath = relative export { resolve } from 'pathe' -export function resetModules(modules: ModuleCacheMap, resetMocks = false) { - const skipPaths = [ - // Vitest - /\/vitest\/dist\//, - /\/vite-node\/dist\//, - // yarn's .store folder - /vitest-virtual-\w+\/dist/, - // cnpm - /@vitest\/dist/, - // don't clear mocks - ...(!resetMocks ? [/^mock:/] : []), - ] - modules.forEach((mod, path) => { - if (skipPaths.some(re => re.test(path))) - return - modules.invalidateModule(mod) - }) -} - export function removeUndefinedValues>(obj: T): T { for (const key in Object.keys(obj)) { if (obj[key] === undefined) diff --git a/packages/vitest/src/utils/memory-limit.ts b/packages/vitest/src/utils/memory-limit.ts new file mode 100644 index 000000000000..581b19f8c68f --- /dev/null +++ b/packages/vitest/src/utils/memory-limit.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of facebook/jest GitHub project tree. + */ + +import { cpus } from 'node:os' +import type { ResolvedConfig } from '../types' + +function getDefaultThreadsCount(config: ResolvedConfig) { + return config.watch + ? Math.max(Math.floor(cpus().length / 2), 1) + : Math.max(cpus().length - 1, 1) +} + +export function getWorkerMemoryLimit(config: ResolvedConfig) { + if (config.experimentalVmWorkerMemoryLimit) + return config.experimentalVmWorkerMemoryLimit + return 1 / (config.maxThreads ?? getDefaultThreadsCount(config)) +} + +/** + * Converts a string representing an amount of memory to bytes. + * + * @param input The value to convert to bytes. + * @param percentageReference The reference value to use when a '%' value is supplied. + */ +export function stringToBytes( + input: string | number | null | undefined, + percentageReference?: number, +): number | null | undefined { + if (input === null || input === undefined) + return input + + if (typeof input === 'string') { + if (Number.isNaN(Number.parseFloat(input.slice(-1)))) { + let [, numericString, trailingChars] + = input.match(/(.*?)([^0-9.-]+)$/i) || [] + + if (trailingChars && numericString) { + const numericValue = Number.parseFloat(numericString) + trailingChars = trailingChars.toLowerCase() + + switch (trailingChars) { + case '%': + input = numericValue / 100 + break + case 'kb': + case 'k': + return numericValue * 1000 + case 'kib': + return numericValue * 1024 + case 'mb': + case 'm': + return numericValue * 1000 * 1000 + case 'mib': + return numericValue * 1024 * 1024 + case 'gb': + case 'g': + return numericValue * 1000 * 1000 * 1000 + case 'gib': + return numericValue * 1024 * 1024 * 1024 + } + } + + // It ends in some kind of char so we need to do some parsing + } + else { + input = Number.parseFloat(input) + } + } + + if (typeof input === 'number') { + if (input <= 1 && input > 0) { + if (percentageReference) { + return Math.floor(input * percentageReference) + } + else { + throw new Error( + 'For a percentage based memory limit a percentageReference must be supplied', + ) + } + } + else if (input > 1) { + return Math.floor(input) + } + else { + throw new Error('Unexpected numerical input for "experimentalVmWorkerMemoryLimit"') + } + } + + return null +} diff --git a/packages/vitest/src/utils/import.ts b/packages/vitest/src/utils/modules.ts similarity index 56% rename from packages/vitest/src/utils/import.ts rename to packages/vitest/src/utils/modules.ts index 0cefc901d256..699d96cffdb1 100644 --- a/packages/vitest/src/utils/import.ts +++ b/packages/vitest/src/utils/modules.ts @@ -1,6 +1,27 @@ +import type { ModuleCacheMap } from 'vite-node/client' + import { getWorkerState } from './global' import { getSafeTimers } from './timers' +export function resetModules(modules: ModuleCacheMap, resetMocks = false) { + const skipPaths = [ + // Vitest + /\/vitest\/dist\//, + /\/vite-node\/dist\//, + // yarn's .store folder + /vitest-virtual-\w+\/dist/, + // cnpm + /@vitest\/dist/, + // don't clear mocks + ...(!resetMocks ? [/^mock:/] : []), + ] + modules.forEach((mod, path) => { + if (skipPaths.some(re => re.test(path))) + return + modules.invalidateModule(mod) + }) +} + function waitNextTick() { const { setTimeout } = getSafeTimers() return new Promise(resolve => setTimeout(resolve, 0)) diff --git a/packages/vitest/suppress-warnings.cjs b/packages/vitest/suppress-warnings.cjs index 9bc4cbb0fdc5..aa71368624b6 100644 --- a/packages/vitest/suppress-warnings.cjs +++ b/packages/vitest/suppress-warnings.cjs @@ -5,6 +5,8 @@ const ignoreWarnings = new Set([ '--experimental-loader is an experimental feature. This feature could change at any time', 'Custom ESM Loaders is an experimental feature. This feature could change at any time', 'Custom ESM Loaders is an experimental feature and might change at any time', + 'VM Modules is an experimental feature and might change at any time', + 'VM Modules is an experimental feature. This feature could change at any time', ]) const { emit } = process diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index 7d4cc38b83ae..dd5f48322b97 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -41,7 +41,7 @@ "typecheck": "tsc --noEmit" }, "peerDependencies": { - "vitest": "*" + "vitest": ">=0.34.0" }, "dependencies": { "debug": "^4.3.4" diff --git a/packages/web-worker/rollup.config.js b/packages/web-worker/rollup.config.js index 06ad151f0e66..93b1482e3bad 100644 --- a/packages/web-worker/rollup.config.js +++ b/packages/web-worker/rollup.config.js @@ -16,7 +16,7 @@ const external = [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {}), 'vitest', - 'vitest/node', + 'vitest/execute', 'vite-node/utils', ] diff --git a/packages/web-worker/src/runner.ts b/packages/web-worker/src/runner.ts index 3f5b7266325a..407d9f60fbd2 100644 --- a/packages/web-worker/src/runner.ts +++ b/packages/web-worker/src/runner.ts @@ -1,4 +1,4 @@ -import { VitestExecutor } from 'vitest/node' +import { VitestExecutor } from 'vitest/execute' export class InlineWorkerRunner extends VitestExecutor { constructor(options: any, private context: any) { diff --git a/packages/web-worker/src/utils.ts b/packages/web-worker/src/utils.ts index 758b357a46b4..a955d02b9de1 100644 --- a/packages/web-worker/src/utils.ts +++ b/packages/web-worker/src/utils.ts @@ -61,7 +61,8 @@ export function createMessageEvent(data: any, transferOrOptions: StructuredSeria } export function getRunnerOptions(): any { - const { config, rpc, mockMap, moduleCache } = getWorkerState() + const state = getWorkerState() + const { config, rpc, mockMap, moduleCache } = state return { fetchModule(id: string) { @@ -76,5 +77,6 @@ export function getRunnerOptions(): any { moduleDirectories: config.deps.moduleDirectories, root: config.root, base: config.base, + state, } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03eaff59afa4..df0195a81c8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,10 +325,10 @@ importers: version: 13.3.0(react-dom@18.0.0)(react@18.0.0) '@types/node': specifier: latest - version: 20.4.1 + version: 20.4.5 '@types/react': specifier: latest - version: 18.2.14 + version: 18.2.17 '@vitejs/plugin-react': specifier: latest version: 4.0.3(vite@4.3.9) @@ -395,7 +395,7 @@ importers: version: link:../../packages/ui happy-dom: specifier: latest - version: 9.20.3 + version: 10.1.1 jsdom: specifier: latest version: 22.1.0 @@ -688,7 +688,7 @@ importers: version: 4.2.3(vite@4.3.9)(vue@3.3.4) '@vue/test-utils': specifier: latest - version: 2.3.2(vue@3.3.4) + version: 2.4.0(vue@3.3.4) jsdom: specifier: latest version: 22.1.0 @@ -734,10 +734,10 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: latest - version: 2.4.2(svelte@3.59.1)(vite@4.3.9) + version: 2.4.3(svelte@4.0.5)(vite@4.3.9) '@testing-library/svelte': - specifier: latest - version: 3.2.2(svelte@3.59.1) + specifier: ^4.0.3 + version: 4.0.3(svelte@4.0.5) '@vitest/ui': specifier: latest version: link:../../packages/ui @@ -746,7 +746,7 @@ importers: version: 22.1.0 svelte: specifier: latest - version: 3.59.1 + version: 4.0.5 vite: specifier: ^4.3.9 version: 4.3.9(@types/node@18.16.19) @@ -804,7 +804,7 @@ importers: version: 22.1.0 unplugin-auto-import: specifier: latest - version: 0.16.4(@vueuse/core@10.2.1)(rollup@3.26.0) + version: 0.16.6(rollup@3.26.0) unplugin-vue-components: specifier: latest version: 0.25.1(rollup@3.26.0)(vue@3.3.4) @@ -847,7 +847,7 @@ importers: version: 3.0.1(vite@4.3.9)(vue@3.3.4) '@vue/test-utils': specifier: latest - version: 2.3.2(vue@3.3.4) + version: 2.4.0(vue@3.3.4) jsdom: specifier: latest version: 22.1.0 @@ -1438,7 +1438,7 @@ importers: specifier: ^4.3.4 version: 4.3.4(supports-color@8.1.1) vitest: - specifier: '*' + specifier: '>=0.34.0' version: link:../vitest devDependencies: '@types/debug': @@ -1600,10 +1600,10 @@ importers: version: link:../../packages/coverage-v8 '@vue/test-utils': specifier: latest - version: 2.3.2(vue@3.3.4) + version: 2.4.0(vue@3.3.4) happy-dom: specifier: latest - version: 9.20.3 + version: 10.1.1 istanbul-lib-coverage: specifier: ^3.2.0 version: 3.2.0 @@ -1660,7 +1660,7 @@ importers: devDependencies: happy-dom: specifier: latest - version: 9.20.3 + version: 10.1.1 vite: specifier: ^4.3.9 version: 4.3.9(@types/node@18.16.19) @@ -2115,6 +2115,7 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.18 + dev: true /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} @@ -2235,6 +2236,10 @@ packages: resolution: {integrity: sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==} dev: true + /@antfu/utils@0.7.5: + resolution: {integrity: sha512-dlR6LdS+0SzOAPx/TPRhnoi7hE251OVeT2Snw0RguNbBSbjUHdWr0l3vcUUDg26rEysT89kCbtw1lVorBXLLCg==} + dev: true + /@apideck/better-ajv-errors@0.3.6(ajv@8.11.0): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -2283,6 +2288,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.22.5 + dev: true /@babel/code-frame@7.22.5: resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} @@ -2339,6 +2345,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: true /@babel/core@7.20.5: resolution: {integrity: sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==} @@ -2392,6 +2399,7 @@ packages: '@babel/types': 7.22.5 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 + dev: true /@babel/generator@7.22.5: resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==} @@ -2428,6 +2436,7 @@ packages: '@babel/helper-validator-option': 7.22.5 browserslist: 4.21.3 semver: 6.3.0 + dev: true /@babel/helper-compilation-targets@7.22.5(@babel/core@7.18.13): resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==} @@ -2657,6 +2666,7 @@ packages: '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color + dev: true /@babel/helper-module-transforms@7.22.5: resolution: {integrity: sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==} @@ -2784,6 +2794,7 @@ packages: '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color + dev: true /@babel/helpers@7.22.5: resolution: {integrity: sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==} @@ -2809,6 +2820,7 @@ packages: hasBin: true dependencies: '@babel/types': 7.22.5 + dev: true /@babel/parser@7.22.5: resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==} @@ -4854,6 +4866,7 @@ packages: '@babel/code-frame': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 + dev: true /@babel/template@7.22.5: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} @@ -4879,6 +4892,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: true /@babel/traverse@7.22.5: resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==} @@ -4904,6 +4918,7 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 + dev: true /@babel/types@7.22.5: resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} @@ -5813,7 +5828,7 @@ packages: resolution: {integrity: sha512-P8S3z/L1LcV4Qem9AoCfVAaTFGySEMzFEY4CHZLkfRj0Fv9LiR+AwjDgrDrzyI93U2L2mg9JHsbTJ52mF8suNw==} dependencies: '@antfu/install-pkg': 0.1.1 - '@antfu/utils': 0.7.4 + '@antfu/utils': 0.7.5 '@iconify/types': 2.0.0 debug: 4.3.4(supports-color@8.1.1) kolorist: 1.8.0 @@ -5842,7 +5857,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 jest-message-util: 27.5.1 jest-util: 27.5.1 @@ -5863,7 +5878,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.8.1 @@ -5900,7 +5915,7 @@ packages: dependencies: '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 jest-mock: 27.5.1 dev: true @@ -5917,7 +5932,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 20.4.1 + '@types/node': 20.4.5 jest-message-util: 27.5.1 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -5946,7 +5961,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -6059,7 +6074,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -6070,7 +6085,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -6082,7 +6097,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -6107,6 +6122,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} @@ -6605,7 +6621,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 playwright-core: 1.28.0 dev: true @@ -8275,7 +8291,7 @@ packages: svelte: ^3.54.0 || ^4.0.0-next.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@3.59.1)(vite@4.3.9) + '@sveltejs/vite-plugin-svelte': 2.4.3(svelte@3.59.1)(vite@4.3.9) '@types/cookie': 0.5.1 cookie: 0.5.0 devalue: 4.3.2 @@ -8294,7 +8310,7 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@3.59.1)(vite@4.3.9): + /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@3.59.1)(vite@4.3.9): resolution: {integrity: sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==} engines: {node: ^14.18.0 || >= 16} peerDependencies: @@ -8302,7 +8318,7 @@ packages: svelte: ^3.54.0 || ^4.0.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@3.59.1)(vite@4.3.9) + '@sveltejs/vite-plugin-svelte': 2.4.3(svelte@3.59.1)(vite@4.3.9) debug: 4.3.4(supports-color@8.1.1) svelte: 3.59.1 vite: 4.3.9(@types/node@18.16.19) @@ -8310,14 +8326,30 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte@2.4.2(svelte@3.59.1)(vite@4.3.9): - resolution: {integrity: sha512-ePfcC48ftMKhkT0OFGdOyycYKnnkT6i/buzey+vHRTR/JpQvuPzzhf1PtKqCDQfJRgoPSN2vscXs6gLigx/zGw==} + /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@4.0.5)(vite@4.3.9): + resolution: {integrity: sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.3(svelte@4.0.5)(vite@4.3.9) + debug: 4.3.4(supports-color@8.1.1) + svelte: 4.0.5 + vite: 4.3.9(@types/node@18.16.19) + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte@2.4.3(svelte@3.59.1)(vite@4.3.9): + resolution: {integrity: sha512-NY2h+B54KHZO3kDURTdARqthn6D4YSIebtfW75NvZ/fwyk4G+AJw3V/i0OBjyN4406Ht9yZcnNWMuRUFnDNNiA==} engines: {node: ^14.18.0 || >= 16} peerDependencies: svelte: ^3.54.0 || ^4.0.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@3.59.1)(vite@4.3.9) + '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@3.59.1)(vite@4.3.9) debug: 4.3.4(supports-color@8.1.1) deepmerge: 4.3.1 kleur: 4.1.5 @@ -8330,6 +8362,26 @@ packages: - supports-color dev: true + /@sveltejs/vite-plugin-svelte@2.4.3(svelte@4.0.5)(vite@4.3.9): + resolution: {integrity: sha512-NY2h+B54KHZO3kDURTdARqthn6D4YSIebtfW75NvZ/fwyk4G+AJw3V/i0OBjyN4406Ht9yZcnNWMuRUFnDNNiA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@4.0.5)(vite@4.3.9) + debug: 4.3.4(supports-color@8.1.1) + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.1 + svelte: 4.0.5 + svelte-hmr: 0.15.2(svelte@4.0.5) + vite: 4.3.9(@types/node@18.16.19) + vitefu: 0.2.4(vite@4.3.9) + transitivePeerDependencies: + - supports-color + dev: true + /@szmarczak/http-timer@5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -8461,14 +8513,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@testing-library/svelte@3.2.2(svelte@3.59.1): - resolution: {integrity: sha512-IKwZgqbekC3LpoRhSwhd0JswRGxKdAGkf39UiDXTywK61YyLXbCYoR831e/UUC6EeNW4hiHPY+2WuovxOgI5sw==} + /@testing-library/svelte@4.0.3(svelte@4.0.5): + resolution: {integrity: sha512-GldAnyGEOn5gMwME+hLVQrnfuKZFB+it5YOMnRBHX+nqeHMsSa18HeqkdvGqtqLpvn81xV7R7EYFb500ngUfXA==} engines: {node: '>= 10'} peerDependencies: - svelte: 3.x + svelte: ^3 || ^4 dependencies: - '@testing-library/dom': 8.19.0 - svelte: 3.59.1 + '@testing-library/dom': 9.3.1 + svelte: 4.0.5 dev: true /@testing-library/user-event@13.5.0(@testing-library/dom@8.17.1): @@ -8584,7 +8636,7 @@ packages: /@types/cheerio@0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/codemirror@5.60.8: @@ -8623,7 +8675,7 @@ packages: resolution: {integrity: sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA==} dependencies: '@types/cheerio': 0.22.31 - '@types/react': 18.2.14 + '@types/react': 18.2.17 dev: true /@types/eslint-scope@3.7.4: @@ -8654,33 +8706,33 @@ packages: resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/glob@8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/graceful-fs@4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/hast@2.3.4: @@ -8752,7 +8804,7 @@ packages: /@types/jsdom@21.1.1: resolution: {integrity: sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -8764,7 +8816,7 @@ packages: /@types/jsonfile@6.1.1: resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/lodash@4.14.195: @@ -8798,7 +8850,7 @@ packages: /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 form-data: 3.0.1 dev: true @@ -8817,8 +8869,8 @@ packages: resolution: {integrity: sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==} dev: false - /@types/node@20.4.1: - resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==} + /@types/node@20.4.5: + resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==} dev: true /@types/normalize-package-data@2.4.1: @@ -8847,7 +8899,7 @@ packages: /@types/prompts@2.4.4: resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 kleur: 3.0.3 dev: true @@ -8871,19 +8923,19 @@ packages: /@types/react-dom@18.0.6: resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} dependencies: - '@types/react': 18.2.14 + '@types/react': 18.2.17 dev: true /@types/react-dom@18.0.8: resolution: {integrity: sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==} dependencies: - '@types/react': 18.2.14 + '@types/react': 18.2.17 dev: true /@types/react-is@17.0.3: resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} dependencies: - '@types/react': 18.2.14 + '@types/react': 18.2.17 dev: false /@types/react-test-renderer@17.0.2: @@ -8895,7 +8947,7 @@ packages: /@types/react-transition-group@4.4.5: resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} dependencies: - '@types/react': 18.2.14 + '@types/react': 18.2.17 dev: false /@types/react@17.0.49: @@ -8914,8 +8966,8 @@ packages: csstype: 3.1.0 dev: true - /@types/react@18.2.14: - resolution: {integrity: sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==} + /@types/react@18.2.17: + resolution: {integrity: sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 @@ -8924,7 +8976,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/resolve@1.20.2: @@ -8941,7 +8993,7 @@ packages: /@types/set-cookie-parser@2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/sinonjs__fake-timers@8.1.1: @@ -8983,7 +9035,7 @@ packages: /@types/through@0.0.30: resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/tough-cookie@4.0.2: @@ -9021,7 +9073,7 @@ packages: /@types/webpack-sources@3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -9029,7 +9081,7 @@ packages: /@types/webpack@4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -9044,7 +9096,7 @@ packages: /@types/ws@8.5.5: resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@types/yargs-parser@21.0.0: @@ -9073,7 +9125,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true optional: true @@ -9098,7 +9150,7 @@ packages: grapheme-splitter: 1.0.4 ignore: 5.2.0 natural-compare-lite: 1.4.0 - semver: 7.5.2 + semver: 7.5.4 tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: @@ -9500,7 +9552,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.5) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.5) react-refresh: 0.14.0 - vite: 4.3.9(@types/node@20.4.1) + vite: 4.3.9(@types/node@20.4.5) transitivePeerDependencies: - supports-color dev: true @@ -9558,7 +9610,7 @@ packages: /@volar/typescript-faster@0.40.13: resolution: {integrity: sha512-uy+TlcFkKoNlKEnxA4x5acxdxLyVDIXGSc8cYDNXpPKjBKXrQaetzCzlO3kVBqu1VLMxKNGJMTKn35mo+ILQmw==} dependencies: - semver: 7.5.2 + semver: 7.5.4 dev: true /@volar/vue-language-core@0.40.13: @@ -9798,16 +9850,21 @@ packages: vue: 3.3.4 dev: true - /@vue/test-utils@2.3.2(vue@3.3.4): - resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} + /@vue/test-utils@2.4.0(vue@3.3.4): + resolution: {integrity: sha512-BKB9aj1yky63/I3IwSr1FjUeHYsKXI7D6S9F378AHt7a5vC0dLkOBtSsFXoRGC/7BfHmiB9HRhT+i9xrUHoAKw==} peerDependencies: + '@vue/compiler-dom': ^3.0.1 + '@vue/server-renderer': ^3.0.1 vue: ^3.0.1 + peerDependenciesMeta: + '@vue/compiler-dom': + optional: true + '@vue/server-renderer': + optional: true dependencies: js-beautify: 1.14.6 vue: 3.3.4 - optionalDependencies: - '@vue/compiler-dom': 3.3.4 - '@vue/server-renderer': 3.3.4(vue@3.3.4) + vue-component-type-helpers: 1.6.5 dev: true /@vueuse/core@10.2.1(vue@3.3.4): @@ -9995,14 +10052,14 @@ packages: resolution: {integrity: sha512-VZ1WFHTNKjR8Ga97TtV2SZM6fvRjWbYI2i/f4pJB4PtusorKvONAMJf2LQcUBIyzbVobqr7KSrcjmSwRolI+yw==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@wdio/types@8.10.4: resolution: {integrity: sha512-aLJ1QQW+hhALeRK3bvMLjIrlUVyhOs3Od+91pR4Z4pLwyeNG1bJZCJRD5bAJK/mm7CnFa0NsdixPS9jJxZcRrw==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /@wdio/utils@8.12.1: @@ -10571,6 +10628,7 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + dev: true /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} @@ -10597,6 +10655,7 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -10992,6 +11051,12 @@ packages: transitivePeerDependencies: - debug + /axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: true + /babel-jest@27.5.1(@babel/core@7.22.5): resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11967,7 +12032,7 @@ packages: engines: {node: '>=12.13.0'} hasBin: true dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.3.0 @@ -12112,6 +12177,7 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: true /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -12146,6 +12212,16 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true + /code-red@1.0.3: + resolution: {integrity: sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.1 + acorn: 8.9.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + dev: true + /codemirror-theme-vars@0.1.2: resolution: {integrity: sha512-WTau8X2q58b0SOAY9DO+iQVw8JKVEgyQIqArp2D732tcc+pobbMta3bnVMdQdmgwuvNrOFFr6HoxPRoQOgooFA==} dev: true @@ -12180,12 +12256,14 @@ packages: engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 + dev: true /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true /color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} @@ -12355,6 +12433,7 @@ packages: resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} dependencies: safe-buffer: 5.1.2 + dev: true /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -12561,6 +12640,7 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + dev: true /crypto-browserify@3.12.0: resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} @@ -13250,7 +13330,7 @@ packages: resolution: {integrity: sha512-R72raQLN1lDSqbr2DVj9SRh07JRyojzmrcLa33VBa2nw3cf5ZyHOHe0DgxlJ/5c2Dfs1+wGNJy16gWKGBq+xgg==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@wdio/config': 8.12.1 '@wdio/logger': 8.11.0 '@wdio/protocols': 8.11.0 @@ -13537,6 +13617,7 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -14417,7 +14498,7 @@ packages: is-core-module: 2.12.1 minimatch: 3.1.2 resolve: 1.22.2 - semver: 7.5.2 + semver: 7.5.4 dev: true /eslint-plugin-no-only-tests@3.1.0: @@ -14455,7 +14536,7 @@ packages: regexp-tree: 0.1.24 regjsparser: 0.10.0 safe-regex: 2.1.1 - semver: 7.5.2 + semver: 7.5.4 strip-indent: 3.0.0 dev: true @@ -14485,7 +14566,7 @@ packages: natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.10 - semver: 7.5.2 + semver: 7.5.4 vue-eslint-parser: 9.3.1(eslint@8.44.0) xml-name-validator: 4.0.0 transitivePeerDependencies: @@ -15297,6 +15378,7 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + dev: true /find-up@6.3.0: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} @@ -15371,6 +15453,7 @@ packages: dependencies: cross-spawn: 7.0.3 signal-exit: 3.0.7 + dev: true /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} @@ -15626,6 +15709,7 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + dev: true /get-func-name@2.0.0: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} @@ -15791,7 +15875,7 @@ packages: dependencies: foreground-child: 3.1.1 jackspeak: 2.1.1 - minimatch: 9.0.1 + minimatch: 9.0.3 minipass: 5.0.0 path-scurry: 1.7.0 dev: true @@ -15973,6 +16057,17 @@ packages: uglify-js: 3.17.0 dev: true + /happy-dom@10.1.1: + resolution: {integrity: sha512-/9AMl/rwMCAz/lAs55W0B2p9I/RfnMWmR2iBI1/twz0+XZYrVgHyJzrBTpHDZlbf00NRk/pFoaktKPtdAP5Tlg==} + dependencies: + css.escape: 1.5.1 + entities: 4.5.0 + iconv-lite: 0.6.3 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + dev: true + /happy-dom@9.20.3: resolution: {integrity: sha512-eBsgauT435fXFvQDNcmm5QbGtYzxEzOaX35Ia+h6yP/wwa4xSWZh1CfP+mGby8Hk6Xu59mTkpyf72rUXHNxY7A==} dependencies: @@ -16812,6 +16907,7 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + dev: true /is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} @@ -17107,6 +17203,7 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true /isobject@2.1.0: resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} @@ -17146,8 +17243,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.18.13 - '@babel/parser': 7.18.13 + '@babel/core': 7.22.5 + '@babel/parser': 7.22.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.0 @@ -17240,7 +17337,7 @@ packages: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -17376,7 +17473,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 jest-mock: 27.5.1 jest-util: 27.5.1 jsdom: 16.7.0 @@ -17394,7 +17491,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 jest-mock: 27.5.1 jest-util: 27.5.1 dev: true @@ -17415,7 +17512,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 20.4.1 + '@types/node': 20.4.5 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -17438,7 +17535,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.5 - '@types/node': 20.4.1 + '@types/node': 20.4.5 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -17478,7 +17575,7 @@ packages: '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -17558,7 +17655,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 dev: true /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): @@ -17619,7 +17716,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 emittery: 0.8.1 graceful-fs: 4.2.10 @@ -17676,7 +17773,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 graceful-fs: 4.2.10 dev: true @@ -17684,7 +17781,7 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 graceful-fs: 4.2.10 dev: true @@ -17723,7 +17820,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -17735,7 +17832,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.10 @@ -17747,7 +17844,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -17772,7 +17869,7 @@ packages: dependencies: '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.4.1 + '@types/node': 20.4.5 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 27.5.1 @@ -17783,7 +17880,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -17792,7 +17889,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -18136,6 +18233,7 @@ packages: resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} engines: {node: '>=6'} hasBin: true + dev: true /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -18149,7 +18247,7 @@ packages: acorn: 8.9.0 eslint-visitor-keys: 3.4.1 espree: 9.5.2 - semver: 7.5.2 + semver: 7.5.4 dev: true /jsonc-parser@3.2.0: @@ -18448,6 +18546,10 @@ packages: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} + /locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + dev: true + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -18468,6 +18570,7 @@ packages: engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + dev: true /locate-path@7.1.1: resolution: {integrity: sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==} @@ -18626,7 +18729,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /lru-cache@9.1.1: resolution: {integrity: sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==} @@ -19028,6 +19130,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} dev: true @@ -19965,6 +20074,7 @@ packages: engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 + dev: true /p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} @@ -19991,6 +20101,7 @@ packages: engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + dev: true /p-locate@6.0.0: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} @@ -20181,6 +20292,7 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: true /path-exists@5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} @@ -20199,6 +20311,7 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + dev: true /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} @@ -21687,6 +21800,7 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} @@ -21983,6 +22097,7 @@ packages: /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -22302,7 +22417,7 @@ packages: detect-libc: 2.0.1 node-addon-api: 6.1.0 prebuild-install: 7.1.1 - semver: 7.5.2 + semver: 7.5.4 simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 @@ -22320,6 +22435,7 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 + dev: true /shebang-regex@1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} @@ -22329,6 +22445,7 @@ packages: /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + dev: true /shell-quote@1.7.4: resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==} @@ -22361,6 +22478,7 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true /signal-exit@4.0.1: resolution: {integrity: sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==} @@ -22820,6 +22938,7 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: true /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} @@ -22919,6 +23038,7 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 + dev: true /strip-ansi@7.0.1: resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} @@ -23154,6 +23274,15 @@ packages: svelte: 3.59.1 dev: true + /svelte-hmr@0.15.2(svelte@4.0.5): + resolution: {integrity: sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0-next.0 + dependencies: + svelte: 4.0.5 + dev: true + /svelte-preprocess@5.0.4(svelte@3.59.1)(typescript@5.1.3): resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} engines: {node: '>= 14.10.0'} @@ -23206,6 +23335,25 @@ packages: engines: {node: '>= 8'} dev: true + /svelte@4.0.5: + resolution: {integrity: sha512-PHKPWP1wiWHBtsE57nCb8xiWB3Ht7/3Kvi3jac0XIxUM2rep8alO7YoAtgWeGD7++tFy46krilOrPW0mG3Dx+A==} + engines: {node: '>=16'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 + acorn: 8.9.0 + aria-query: 5.3.0 + axobject-query: 3.2.1 + code-red: 1.0.3 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.1 + locate-character: 3.0.0 + magic-string: 0.30.1 + periscopic: 3.1.0 + dev: true + /sveltedoc-parser@4.2.1: resolution: {integrity: sha512-sWJRa4qOfRdSORSVw9GhfDEwsbsYsegnDzBevUCF6k/Eis/QqCu9lJ6I0+d/E2wOWCjOhlcJ3+jl/Iur+5mmCw==} engines: {node: '>=10.0.0'} @@ -23934,7 +24082,7 @@ packages: /unconfig@0.3.9: resolution: {integrity: sha512-8yhetFd48M641mxrkWA+C/lZU4N0rCOdlo3dFsyFPnBHBjMJfjT/3eAZBRT2RxCRqeBMAKBVgikejdS6yeBjMw==} dependencies: - '@antfu/utils': 0.7.4 + '@antfu/utils': 0.7.5 defu: 6.1.2 jiti: 1.18.2 dev: true @@ -23999,6 +24147,24 @@ packages: vfile: 4.2.1 dev: true + /unimport@3.0.14(rollup@3.26.0): + resolution: {integrity: sha512-67Rh/sGpEuVqdHWkXaZ6NOq+I7sKt86o+DUtKeGB6dh4Hk1A8AQrzyVGg2+LaVEYotStH7HwvV9YSaRjyT7Uqg==} + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.26.0) + escape-string-regexp: 5.0.0 + fast-glob: 3.3.0 + local-pkg: 0.4.3 + magic-string: 0.30.1 + mlly: 1.4.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + scule: 1.0.0 + strip-literal: 1.0.1 + unplugin: 1.3.2 + transitivePeerDependencies: + - rollup + dev: true + /unimport@3.0.7(rollup@3.26.0): resolution: {integrity: sha512-2dVQUxJEGcrSZ0U4qtwJVODrlfyGcwmIOoHVqbAFFUx7kPoEN5JWr1cZFhLwoAwTmZOvqAm3YIkzv1engIQocg==} dependencies: @@ -24012,7 +24178,7 @@ packages: pkg-types: 1.0.3 scule: 1.0.0 strip-literal: 1.0.1 - unplugin: 1.3.1 + unplugin: 1.3.2 transitivePeerDependencies: - rollup dev: true @@ -24213,6 +24379,30 @@ packages: - rollup dev: true + /unplugin-auto-import@0.16.6(rollup@3.26.0): + resolution: {integrity: sha512-M+YIITkx3C/Hg38hp8HmswP5mShUUyJOzpifv7RTlAbeFlO2Tyw0pwrogSSxnipHDPTtI8VHFBpkYkNKzYSuyA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + dependencies: + '@antfu/utils': 0.7.5 + '@rollup/pluginutils': 5.0.2(rollup@3.26.0) + fast-glob: 3.3.0 + local-pkg: 0.4.3 + magic-string: 0.30.1 + minimatch: 9.0.3 + unimport: 3.0.14(rollup@3.26.0) + unplugin: 1.3.2 + transitivePeerDependencies: + - rollup + dev: true + /unplugin-vue-components@0.25.1(rollup@2.79.1)(vue@3.3.4): resolution: {integrity: sha512-kzS2ZHVMaGU2XEO2keYQcMjNZkanDSGDdY96uQT9EPe+wqSZwwgbFfKVJ5ti0+8rGAcKHColwKUvctBhq2LJ3A==} engines: {node: '>=14'} @@ -24226,16 +24416,16 @@ packages: '@nuxt/kit': optional: true dependencies: - '@antfu/utils': 0.7.4 + '@antfu/utils': 0.7.5 '@rollup/pluginutils': 5.0.2(rollup@2.79.1) chokidar: 3.5.3 debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.0 local-pkg: 0.4.3 - magic-string: 0.30.0 - minimatch: 9.0.1 + magic-string: 0.30.1 + minimatch: 9.0.3 resolve: 1.22.2 - unplugin: 1.3.1 + unplugin: 1.3.2 vue: 3.3.4 transitivePeerDependencies: - rollup @@ -24255,16 +24445,16 @@ packages: '@nuxt/kit': optional: true dependencies: - '@antfu/utils': 0.7.4 + '@antfu/utils': 0.7.5 '@rollup/pluginutils': 5.0.2(rollup@3.26.0) chokidar: 3.5.3 debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.0 local-pkg: 0.4.3 magic-string: 0.30.1 - minimatch: 9.0.1 + minimatch: 9.0.3 resolve: 1.22.2 - unplugin: 1.3.1 + unplugin: 1.3.2 vue: 3.3.4 transitivePeerDependencies: - rollup @@ -24280,6 +24470,15 @@ packages: webpack-virtual-modules: 0.5.0 dev: true + /unplugin@1.3.2: + resolution: {integrity: sha512-Lh7/2SryjXe/IyWqx9K7IKwuKhuOFZEhotiBquOODsv2IVyDkI9lv/XhgfjdXf/xdbv32txmnBNnC/JVTDJlsA==} + dependencies: + acorn: 8.9.0 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + dev: true + /unset-value@1.0.0: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} engines: {node: '>=0.10.0'} @@ -24639,7 +24838,7 @@ packages: fsevents: 2.3.2 dev: false - /vite@4.3.9(@types/node@20.4.1): + /vite@4.3.9(@types/node@20.4.5): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -24664,7 +24863,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 esbuild: 0.17.18 postcss: 8.4.24 rollup: 3.23.0 @@ -24759,6 +24958,10 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: true + /vue-component-type-helpers@1.6.5: + resolution: {integrity: sha512-iGdlqtajmiqed8ptURKPJ/Olz0/mwripVZszg6tygfZSIL9kYFPJTNY6+Q6OjWGznl2L06vxG5HvNvAnWrnzbg==} + dev: true + /vue-demi@0.13.11(vue@3.2.39): resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} engines: {node: '>=12'} @@ -24972,7 +25175,7 @@ packages: resolution: {integrity: sha512-Ca+MUYUXfl5gsnX40xAIUgfoa76qQsfX7REGFzMl09Cb7vHKtM17bEOGDaTbXIX4kbkXylyUSAuBpe3gCtDDKg==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@types/ws': 8.5.5 '@wdio/config': 8.12.1 '@wdio/logger': 8.11.0 @@ -24992,7 +25195,7 @@ packages: resolution: {integrity: sha512-lW0Qo3fy64cVbYWWAZbXxLIOK0pbTARgpY89J+0Sr6zh2K2NKtd/0D11k3WfMeYxd0b0he7E7XC1b6M6w4h75A==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 20.4.1 + '@types/node': 20.4.5 '@wdio/config': 8.12.1 '@wdio/logger': 8.11.0 '@wdio/protocols': 8.11.0 @@ -25010,7 +25213,7 @@ packages: is-plain-obj: 4.1.0 lodash.clonedeep: 4.5.0 lodash.zip: 4.2.0 - minimatch: 9.0.1 + minimatch: 9.0.3 puppeteer-core: 20.3.0(typescript@5.1.6) query-selector-shadow-dom: 1.0.1 resq: 1.11.0 @@ -25295,6 +25498,7 @@ packages: hasBin: true dependencies: isexe: 2.0.0 + dev: true /which@3.0.0: resolution: {integrity: sha512-nla//68K9NU6yRiwDY/Q8aU6siKlSs64aEC7+IV56QoAuyQT2ovsJcgGYGyqMOmI/CGN1BOR6mM5EN0FBO+zyQ==} @@ -25509,6 +25713,7 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + dev: true /wrap-ansi@8.0.1: resolution: {integrity: sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==} @@ -25614,6 +25819,7 @@ packages: /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + dev: true /yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} @@ -25624,7 +25830,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml-eslint-parser@1.2.2: resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} @@ -25647,6 +25852,7 @@ packages: /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + dev: true /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} @@ -25664,6 +25870,7 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 + dev: true /yargs@17.5.1: resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==} @@ -25706,6 +25913,7 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + dev: true /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} diff --git a/test/core/src/add.wasm b/test/core/src/add.wasm new file mode 100644 index 000000000000..357f72da7a0d Binary files /dev/null and b/test/core/src/add.wasm differ diff --git a/test/core/src/wasm-bindgen/index.js b/test/core/src/wasm-bindgen/index.js new file mode 100644 index 000000000000..835dc8671064 --- /dev/null +++ b/test/core/src/wasm-bindgen/index.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// folder source: https://github.com/rustwasm/wasm-bindgen/tree/4f865308afbe8d2463968457711ad356bae63b71/examples/hello_world +// docs: https://rustwasm.github.io/docs/wasm-bindgen/examples/hello-world.html + +// eslint-disable-next-line unused-imports/no-unused-imports, import/newline-after-import +import * as wasm from './index_bg.wasm' +export * from './index_bg.js' diff --git a/test/core/src/wasm-bindgen/index_bg.js b/test/core/src/wasm-bindgen/index_bg.js new file mode 100644 index 000000000000..45b649d8bfbb --- /dev/null +++ b/test/core/src/wasm-bindgen/index_bg.js @@ -0,0 +1,148 @@ +/* eslint-disable no-alert */ +/* eslint-disable prefer-rest-params */ + +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as wasm from './index_bg.wasm' + +const LTextDecoder + = typeof TextDecoder === 'undefined' + ? (0, module.require)('util').TextDecoder + : TextDecoder + +const cachedTextDecoder = new LTextDecoder('utf-8', { + fatal: true, + ignoreBOM: true, +}) + +cachedTextDecoder.decode() + +let cachedUint8Memory0 = new Uint8Array() + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer) + + return cachedUint8Memory0 +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)) +} + +function logError(f, args) { + try { + return f.apply(this, args) + } + catch (e) { + const error = (function () { + try { + return e instanceof Error + ? `${e.message}\n\nStack:\n${e.stack}` + : e.toString() + } + catch (_) { + return '' + } + })() + console.error( + 'wasm-bindgen: imported JS function that was not marked as `catch` threw an error:', + error, + ) + throw e + } +} + +let WASM_VECTOR_LEN = 0 + +const LTextEncoder + = typeof TextEncoder === 'undefined' + ? (0, module.require)('util').TextEncoder + : TextEncoder + +const cachedTextEncoder = new LTextEncoder('utf-8') + +const encodeString + = typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view) + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg) + view.set(buf) + return { + read: arg.length, + written: buf.length, + } + } + +function passStringToWasm0(arg, malloc, realloc) { + if (typeof arg !== 'string') + throw new Error('expected a string argument') + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg) + const ptr = malloc(buf.length) + getUint8Memory0() + .subarray(ptr, ptr + buf.length) + .set(buf) + WASM_VECTOR_LEN = buf.length + return ptr + } + + let len = arg.length + let ptr = malloc(len) + + const mem = getUint8Memory0() + + let offset = 0 + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset) + if (code > 0x7F) + break + mem[ptr + offset] = code + } + + if (offset !== len) { + if (offset !== 0) + arg = arg.slice(offset) + + ptr = realloc(ptr, len, (len = offset + arg.length * 3)) + const view = getUint8Memory0().subarray(ptr + offset, ptr + len) + const ret = encodeString(arg, view) + if (ret.read !== arg.length) + throw new Error('failed to pass whole string') + offset += ret.written + } + + WASM_VECTOR_LEN = offset + return ptr +} +/** + * @param {string} name + */ +export function greet(name) { + const ptr0 = passStringToWasm0( + name, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ) + const len0 = WASM_VECTOR_LEN + wasm.greet(ptr0, len0) +} + +export function __wbg_alert_9ea5a791b0d4c7a3() { + return logError((arg0, arg1) => { + alert(getStringFromWasm0(arg0, arg1)) + }, arguments) +} + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)) +} diff --git a/test/core/src/wasm-bindgen/index_bg.wasm b/test/core/src/wasm-bindgen/index_bg.wasm new file mode 100644 index 000000000000..e545fdfd14d3 Binary files /dev/null and b/test/core/src/wasm-bindgen/index_bg.wasm differ diff --git a/test/core/src/wasm-bindgen/package.json b/test/core/src/wasm-bindgen/package.json new file mode 100644 index 000000000000..3dbc1ca591c0 --- /dev/null +++ b/test/core/src/wasm-bindgen/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/core/test/happy-dom.test.ts b/test/core/test/happy-dom.test.ts index bef420b134a8..d234ec8ae016 100644 --- a/test/core/test/happy-dom.test.ts +++ b/test/core/test/happy-dom.test.ts @@ -110,6 +110,9 @@ it('globals are the same', () => { expect(window.Blob).toBe(globalThis.Blob) expect(window.globalThis.Blob).toBe(globalThis.Blob) expect(Blob).toBe(globalThis.Blob) +}) + +it.skipIf(import.meta.env.VITEST_VM_POOL)('default view references global object', () => { expect(document.defaultView).toBe(window) expect(document.defaultView).toBe(globalThis) const el = document.createElement('div') diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index eb2d8ce75b14..02be6726a834 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -687,24 +687,24 @@ describe('async expect', () => { try { expect(actual).toBe({ ...actual }) } - catch (error) { - expect(error).toEqual(toStrictEqualError1) + catch (error: any) { + expect(error.message).toBe(toStrictEqualError1.message) } const toStrictEqualError2 = generatedToBeMessage('toStrictEqual', 'FakeClass{}', 'FakeClass{}') try { expect(new FakeClass()).toBe(new FakeClass()) } - catch (error) { - expect(error).toEqual(toStrictEqualError2) + catch (error: any) { + expect(error.message).toBe(toStrictEqualError2.message) } const toEqualError1 = generatedToBeMessage('toEqual', '{}', 'FakeClass{}') try { expect({}).toBe(new FakeClass()) } - catch (error) { - expect(error).toEqual(toEqualError1) + catch (error: any) { + expect(error.message).toBe(toEqualError1.message) // expect(error).toEqual('1234') } @@ -712,8 +712,8 @@ describe('async expect', () => { try { expect(new FakeClass()).toBe({}) } - catch (error) { - expect(error).toEqual(toEqualError2) + catch (error: any) { + expect(error.message).toBe(toEqualError2.message) } }) diff --git a/test/core/test/require.test.ts b/test/core/test/require.test.ts index ba06ddc9cf5e..4d66074db582 100644 --- a/test/core/test/require.test.ts +++ b/test/core/test/require.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment jsdom + import { describe, expect, it } from 'vitest' const _require = require diff --git a/test/core/test/vm-wasm.test.ts b/test/core/test/vm-wasm.test.ts new file mode 100644 index 000000000000..ec1d851bc697 --- /dev/null +++ b/test/core/test/vm-wasm.test.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:url' +import { expect, test, vi } from 'vitest' + +// TODO: currently not supported + +// @ts-expect-error wasm is not typed +import { add } from '../src/add.wasm' + +const wasmFileBuffer = readFileSync(resolve(__dirname, './src/add.wasm')) + +test('supports native wasm imports', () => { + expect(add(1, 2)).toBe(3) + + // because arguments are i32 (signed), fractional part is truncated + expect(add(0.99, 1.01)).toBe(1) + + // because return value is i32 (signed), (2^31 - 1) + 1 overflows and becomes -2^31 + expect(add(2 ** 31 - 1, 1)).toBe(-(2 ** 31)) + + // invalid or missing arguments are treated as 0 + expect(add('hello', 'world')).toBe(0) + expect(add()).toBe(0) + expect(add(null)).toBe(0) + expect(add({}, [])).toBe(0) + + // redundant arguments are silently ignored + expect(add(1, 2, 3)).toBe(3) +}) + +test('supports dynamic wasm imports', async () => { + // @ts-expect-error wasm is not typed + const { add: dynamicAdd } = await import('../src/add.wasm') + expect(dynamicAdd(1, 2)).toBe(3) +}) + +test('supports imports from "data:application/wasm" URI with base64 encoding', async () => { + const importedWasmModule = await import( + `data:application/wasm;base64,${wasmFileBuffer.toString('base64')}` + ) + expect(importedWasmModule.add(0, 42)).toBe(42) +}) + +test('imports from "data:application/wasm" URI without explicit encoding fail', async () => { + await expect(() => + import(`data:application/wasm,${wasmFileBuffer.toString('base64')}`), + ).rejects.toThrow('Missing data URI encoding') +}) + +test('imports from "data:application/wasm" URI with invalid encoding fail', async () => { + await expect(() => + // @ts-expect-error import is not typed + import('data:application/wasm;charset=utf-8,oops'), + ).rejects.toThrow('Invalid data URI encoding: charset=utf-8') +}) + +test('supports wasm files that import js resources (wasm-bindgen)', async () => { + globalThis.alert = vi.fn() + + // @ts-expect-error not typed + const { greet } = await import('../src/wasm-bindgen/index.js') + greet('World') + + expect(globalThis.alert).toHaveBeenCalledWith('Hello, World!') +}) diff --git a/test/core/vitest.config.ts b/test/core/vitest.config.ts index 9da89210a05d..03a6bec0c909 100644 --- a/test/core/vitest.config.ts +++ b/test/core/vitest.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ }, test: { name: 'core', - exclude: ['**/fixtures/**', ...defaultExclude], + exclude: ['**/fixtures/**', '**/vm-wasm.test.ts', ...defaultExclude], slowTestThreshold: 1000, testTimeout: 2000, setupFiles: [ @@ -57,6 +57,9 @@ export default defineConfig({ env: { CUSTOM_ENV: 'foo', }, + poolMatchGlobs: [ + ['**/vm-wasm.test.ts', 'experimentalVmThreads'], + ], resolveSnapshotPath: (path, extension) => { if (path.includes('moved-snapshot')) return path + extension @@ -70,7 +73,7 @@ export default defineConfig({ }, server: { deps: { - external: ['tinyspy', /src\/external/, /esm\/esm/], + external: ['tinyspy', /src\/external/, /esm\/esm/, /\.wasm$/], inline: ['inline-lib'], }, }, diff --git a/test/env-custom/vitest-environment-custom/index.mjs b/test/env-custom/vitest-environment-custom/index.mjs index 7fc3b0755779..da512a72ebda 100644 --- a/test/env-custom/vitest-environment-custom/index.mjs +++ b/test/env-custom/vitest-environment-custom/index.mjs @@ -1,5 +1,25 @@ +import vm from 'node:vm' + export default { name: 'custom', + transformMode: 'ssr', + setupVM({ custom }) { + const context = vm.createContext({ + testEnvironment: 'custom', + option: custom.option, + setTimeout, + clearTimeout, + }) + return { + getVmContext() { + return context + }, + teardown() { + delete context.testEnvironment + delete context.option + }, + } + }, setup(global, { custom }) { global.testEnvironment = 'custom' global.option = custom.option diff --git a/test/public-api/tests/runner.spec.ts b/test/public-api/tests/runner.spec.ts index f81fc719043e..bf177e02382b 100644 --- a/test/public-api/tests/runner.spec.ts +++ b/test/public-api/tests/runner.spec.ts @@ -18,7 +18,7 @@ it.each([ ] as UserConfig[])('passes down metadata when $name', async (config) => { const taskUpdate: TaskResultPack[] = [] const finishedFiles: File[] = [] - const { vitest, stdout } = await runVitest({ + const { vitest, stdout, stderr } = await runVitest({ root: resolve(__dirname, '..', 'fixtures'), include: ['**/*.spec.ts'], reporters: [ @@ -35,6 +35,8 @@ it.each([ ...config, }) + expect(stderr).toBe('') + expect(stdout).toContain('custom.spec.ts > custom') const suiteMeta = { done: true } diff --git a/test/web-worker/test/init.test.ts b/test/web-worker/test/init.test.ts index 27f7434e5167..8b75947feb26 100644 --- a/test/web-worker/test/init.test.ts +++ b/test/web-worker/test/init.test.ts @@ -65,7 +65,9 @@ it('worker with invalid url throws an error', async () => { } }) expect(event).toBeInstanceOf(ErrorEvent) - expect(event.error).toBeInstanceOf(Error) + // Error is in different context when running in VM. This is consistent with jest. + if (!import.meta.env.VITEST_VM_POOL) + expect(event.error).toBeInstanceOf(Error) expect(event.error.message).toContain('Failed to load') }) diff --git a/test/web-worker/test/sharedWorker.spec.ts b/test/web-worker/test/sharedWorker.spec.ts index 792289f478b0..c245d4b5976c 100644 --- a/test/web-worker/test/sharedWorker.spec.ts +++ b/test/web-worker/test/sharedWorker.spec.ts @@ -49,7 +49,9 @@ it('throws an error on invalid path', async () => { } }) expect(event).toBeInstanceOf(ErrorEvent) - expect(event.error).toBeInstanceOf(Error) + // Error is in different context when running in VM. This is consistent with jest. + if (!import.meta.env.VITEST_VM_POOL) + expect(event.error).toBeInstanceOf(Error) expect(event.error.message).toContain('Failed to load') }) diff --git a/test/web-worker/vitest.config.ts b/test/web-worker/vitest.config.ts index 69f70c696d4d..dbb48563fd62 100644 --- a/test/web-worker/vitest.config.ts +++ b/test/web-worker/vitest.config.ts @@ -6,10 +6,12 @@ export default defineConfig({ setupFiles: [ './setup.ts', ], - deps: { - external: [ - /packages\/web-worker/, - ], + server: { + deps: { + external: [ + /packages\/web-worker/, + ], + }, }, onConsoleLog(log) { if (log.includes('Failed to load')) diff --git a/tsconfig.json b/tsconfig.json index f4b1805c6367..6077cbb2d44d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "vitest": ["./packages/vitest/src/index.ts"], "vitest/globals": ["./packages/vitest/globals.d.ts"], "vitest/node": ["./packages/vitest/src/node/index.ts"], + "vitest/execute": ["./packages/vitest/src/public/execute.ts"], "vitest/config": ["./packages/vitest/src/config.ts"], "vitest/coverage": ["./packages/vitest/src/coverage.ts"], "vitest/browser": ["./packages/vitest/src/browser.ts"],