From b09298528b947ae10278c0c666ddc549b5a29eeb Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 1 Aug 2023 08:59:55 +0200 Subject: [PATCH] feat!: support running tests using VM context (#3203) Co-authored-by: Anjorin Damilare --- .github/workflows/ci.yml | 3 + docs/config/index.md | 74 ++- docs/guide/cli.md | 5 +- docs/guide/environment.md | 20 +- examples/mocks/vite.config.ts | 6 +- .../test/__snapshots__/Hello.test.jsx.snap | 2 +- examples/svelte/package.json | 2 +- examples/vitesse/src/auto-import.d.ts | 1 + package.json | 1 + packages/browser/src/client/main.ts | 3 + packages/runner/src/types/runner.ts | 2 +- packages/vite-node/package.json | 5 + packages/vite-node/rollup.config.js | 2 + packages/vite-node/src/client.ts | 58 +- packages/vite-node/src/constants.ts | 38 ++ packages/vite-node/src/externalize.ts | 36 +- packages/vite-node/src/utils.ts | 48 +- packages/vitest/execute.d.ts | 1 + packages/vitest/package.json | 4 + packages/vitest/rollup.config.js | 5 + .../vitest/src/integrations/chai/config.ts | 7 + .../vitest/src/integrations/chai/index.ts | 9 +- .../src/integrations/env/edge-runtime.ts | 18 + .../vitest/src/integrations/env/happy-dom.ts | 21 + packages/vitest/src/integrations/env/index.ts | 27 +- packages/vitest/src/integrations/env/jsdom.ts | 56 ++ packages/vitest/src/integrations/env/node.ts | 108 +++ packages/vitest/src/integrations/run-once.ts | 2 +- .../snapshot/environments/node.ts | 8 +- packages/vitest/src/integrations/vi.ts | 7 +- packages/vitest/src/node/cli-api.ts | 3 + packages/vitest/src/node/cli.ts | 2 + packages/vitest/src/node/config.ts | 8 +- packages/vitest/src/node/index.ts | 3 - packages/vitest/src/node/pool.ts | 15 +- packages/vitest/src/node/pools/child.ts | 2 + packages/vitest/src/node/pools/vm-threads.ts | 156 +++++ packages/vitest/src/public/execute.ts | 1 + packages/vitest/src/runtime/child.ts | 62 +- packages/vitest/src/runtime/console.ts | 112 ++++ packages/vitest/src/runtime/entry-vm.ts | 57 ++ packages/vitest/src/runtime/entry.ts | 92 +-- packages/vitest/src/runtime/execute.ts | 185 +++++- .../vitest/src/runtime/external-executor.ts | 624 ++++++++++++++++++ packages/vitest/src/runtime/mocker.ts | 60 +- packages/vitest/src/runtime/rpc.ts | 9 +- packages/vitest/src/runtime/runners/index.ts | 84 +++ packages/vitest/src/runtime/setup.node.ts | 139 +--- packages/vitest/src/runtime/vm.ts | 119 ++++ packages/vitest/src/runtime/worker.ts | 50 +- packages/vitest/src/types/config.ts | 23 +- packages/vitest/src/types/general.ts | 10 +- packages/vitest/src/types/index.ts | 20 +- packages/vitest/src/types/rpc.ts | 7 +- packages/vitest/src/types/worker.ts | 8 +- packages/vitest/src/utils/base.ts | 20 +- packages/vitest/src/utils/global.ts | 4 +- packages/vitest/src/utils/index.ts | 22 +- packages/vitest/src/utils/memory-limit.ts | 94 +++ .../src/utils/{import.ts => modules.ts} | 21 + packages/vitest/suppress-warnings.cjs | 2 + packages/web-worker/package.json | 2 +- packages/web-worker/rollup.config.js | 2 +- packages/web-worker/src/runner.ts | 2 +- packages/web-worker/src/utils.ts | 4 +- pnpm-lock.yaml | 434 ++++++++---- test/core/src/add.wasm | Bin 0 -> 41 bytes test/core/src/wasm-bindgen/index.js | 13 + test/core/src/wasm-bindgen/index_bg.js | 148 +++++ test/core/src/wasm-bindgen/index_bg.wasm | Bin 0 -> 59838 bytes test/core/src/wasm-bindgen/package.json | 3 + test/core/test/happy-dom.test.ts | 3 + test/core/test/jest-expect.test.ts | 16 +- test/core/test/require.test.ts | 2 + test/core/test/vm-wasm.test.ts | 65 ++ test/core/vitest.config.ts | 7 +- .../vitest-environment-custom/index.mjs | 20 + test/public-api/tests/runner.spec.ts | 4 +- test/web-worker/test/init.test.ts | 4 +- test/web-worker/test/sharedWorker.spec.ts | 4 +- test/web-worker/vitest.config.ts | 10 +- tsconfig.json | 1 + 82 files changed, 2750 insertions(+), 587 deletions(-) create mode 100644 packages/vite-node/src/constants.ts create mode 100644 packages/vitest/execute.d.ts create mode 100644 packages/vitest/src/integrations/chai/config.ts create mode 100644 packages/vitest/src/node/pools/vm-threads.ts create mode 100644 packages/vitest/src/public/execute.ts create mode 100644 packages/vitest/src/runtime/console.ts create mode 100644 packages/vitest/src/runtime/entry-vm.ts create mode 100644 packages/vitest/src/runtime/external-executor.ts create mode 100644 packages/vitest/src/runtime/runners/index.ts create mode 100644 packages/vitest/src/runtime/vm.ts create mode 100644 packages/vitest/src/utils/memory-limit.ts rename packages/vitest/src/utils/{import.ts => modules.ts} (56%) create mode 100644 test/core/src/add.wasm create mode 100644 test/core/src/wasm-bindgen/index.js create mode 100644 test/core/src/wasm-bindgen/index_bg.js create mode 100644 test/core/src/wasm-bindgen/index_bg.wasm create mode 100644 test/core/src/wasm-bindgen/package.json create mode 100644 test/core/test/vm-wasm.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc4811c66..57e8b3852 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 5722359d1..df930e232 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 11a3ad0ea..818f793d5 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 dfd365fa0..b5494c109 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 76cb1dad1..ee914b24a 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 018959cb7..2f7d062f3 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 a1b2bde25..3fdbc671f 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 4259e89c6..9959df651 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 3b9ee959a..f967a870b 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 046dbe7f9..47171f97c 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 eb1ee9edb..25142152f 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 f0f31f22e..641969e28 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 7397ba5b4..258770c42 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 d2814e122..2c97f24f6 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 000000000..9d098cb66 --- /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 ba7bc1c12..7dcf090e3 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 1f2b76362..fdd349db9 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 000000000..9901479f9 --- /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 7d21ab7d2..9cc26025a 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 63d3cb657..3f7ab46a8 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 000000000..2adbcb614 --- /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 93d3c6d80..b25dc2f60 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 9d072eb0a..9aa3a0fb6 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 0a0cb1cc8..3df3b2bd3 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 0cbd7261d..3015f9ddb 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 f197e9c34..30910b082 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 ef1a01cb1..4ba49e471 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 52e391a47..be150ed08 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 59657bb79..739501a67 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 1fcc6f30d..a93cab706 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 a64561389..0592f1ecb 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 c4573ed3c..25872408d 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 50ea5fd85..169c1f36a 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 80b128a4d..bddc19919 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 dcfa13a14..329f16888 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 8ba9fe620..a22e91fb3 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 000000000..5cc867834 --- /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 000000000..05fef4814 --- /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 3dcd5874a..5bdf6cd98 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 000000000..3061221f7 --- /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 000000000..78edc1a32 --- /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 a2c9edaea..0dffd3f2b 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 cbce90c32..1617a4793 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 000000000..dcb903b7a --- /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 4ac7f98a7..adfd9485e 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 2e50bbb9b..0357c059f 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 000000000..761c43114 --- /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 15625fcad..2e1a1aa3e 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 000000000..6c016c1c5 --- /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 ce2fa1a8b..7bb1118c6 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 92e20d67f..5b0b80bd1 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 7e8da33bd..776ac609a 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 011173794..e0d92a8ed 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 6f875b088..9d0721b88 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 b6224e817..7d3c4cde3 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 010a1b0e3..151de2a8c 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 02db76c68..ea7398b6e 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 7cf3ea954..5464456d1 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 000000000..581b19f8c --- /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 0cefc901d..699d96cff 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 9bc4cbb0f..aa7136862 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 7d4cc38b8..dd5f48322 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 06ad151f0..93b1482e3 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 3f5b72663..407d9f60f 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 758b357a4..a955d02b9 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 03eaff59a..df0195a81 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 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/test/core/src/wasm-bindgen/index.js b/test/core/src/wasm-bindgen/index.js new file mode 100644 index 000000000..835dc8671 --- /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 000000000..45b649d8b --- /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 0000000000000000000000000000000000000000..e545fdfd14d352f0612be501c397c367013def38 GIT binary patch literal 59838 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 KOT#o{3rzqR$}))n literal 0 HcmV?d00001 diff --git a/test/core/src/wasm-bindgen/package.json b/test/core/src/wasm-bindgen/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /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 bef420b13..d234ec8ae 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 eb2d8ce75..02be6726a 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 ba06ddc9c..4d66074db 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 000000000..ec1d851bc --- /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 9da89210a..03a6bec0c 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 7fc3b0755..da512a72e 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 f81fc7190..bf177e023 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 27f7434e5..8b75947fe 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 792289f47..c245d4b59 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 69f70c696..dbb48563f 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 f4b1805c6..6077cbb2d 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"], -- 2.51.2