Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: support running tests using VM context #3203

Merged
merged 87 commits into from Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
b0c810d
feat: support running tests using VM context
sheremet-va Apr 17, 2023
e379319
refactor: move "native" logic into a separate class
sheremet-va Apr 18, 2023
f8a7e1c
IT IS ALIVE
sheremet-va Apr 18, 2023
a0de65c
chore: cleanup
sheremet-va Apr 18, 2023
5ff7d01
refactor: cleanup
sheremet-va Apr 18, 2023
2ac7127
chore: pass down state
sheremet-va Apr 18, 2023
89c4210
chore: cleanup
sheremet-va Apr 18, 2023
169fd8e
chore: setup console
sheremet-va Apr 18, 2023
5d8c9f7
chore: cleanup
sheremet-va Apr 18, 2023
ad69374
chore: clear require cache
sheremet-va Apr 18, 2023
a95f147
chore: fix linking race conditions
sheremet-va Apr 18, 2023
f35f320
chore: fix mocker VM bugs
sheremet-va Apr 18, 2023
b9c1dce
chore: cleanup
sheremet-va Apr 18, 2023
755f401
chore: fix evaluating scripts with // comments at the end of the file
sheremet-va Apr 18, 2023
7965255
perf: improve entry import performance
sheremet-va Apr 18, 2023
2c3a913
chore: cleanup
sheremet-va Apr 19, 2023
38a4114
chore: safe guard rpc just once
sheremet-va Apr 19, 2023
320aac2
chore: inline vm types
sheremet-va Apr 19, 2023
a994b93
chore: lockfile
sheremet-va Apr 19, 2023
5f81cf1
chore: cleanup
sheremet-va Apr 19, 2023
e858784
chore: fix web-worker runner options
sheremet-va Apr 19, 2023
b9e4794
refactor: rename vm to experimental-vm-threads
sheremet-va Apr 20, 2023
be23d15
chore: cleanup
sheremet-va May 10, 2023
8264ec6
chore: cleanup
sheremet-va May 10, 2023
57a25b1
chore: process unknown vm pools
sheremet-va May 10, 2023
beeb854
chore: add experimentalVmWorkerMemoryLimit option
sheremet-va May 10, 2023
8e90885
chore: remove debug
sheremet-va May 11, 2023
6386891
chore: copy jest's node env
sheremet-va May 11, 2023
ef98c99
chore: cleanup
sheremet-va May 11, 2023
d0980d8
chore: cleanup
sheremet-va Jun 19, 2023
4096646
chore: add support for require.extensions
sheremet-va Jun 19, 2023
67c2b98
chore: fix caching
sheremet-va Jun 19, 2023
eb3a5a2
chore: remove import.meta.resolve from vite-node deps
sheremet-va Jun 19, 2023
da29e4c
chore: fix types
sheremet-va Jun 19, 2023
f08ebaa
chore: fix cached vite-node
sheremet-va Jun 22, 2023
8ca3a1e
chore: fix lockfile
sheremet-va Jun 22, 2023
6efb225
chore: cleanup
sheremet-va Jun 22, 2023
ee3a95c
chore: cleanup
sheremet-va Jun 22, 2023
f7aa869
chore: cleanup
sheremet-va Jun 22, 2023
19b2752
chore: support resolve
sheremet-va Jul 6, 2023
dae6c8f
chore: cleanup
sheremet-va Jul 11, 2023
5aa323b
chore: fix environment
sheremet-va Jul 11, 2023
31cc8fa
fix: revert satisfies
sheremet-va Jul 11, 2023
2f2580a
fix: improve type module check
sheremet-va Jul 11, 2023
385bb5a
chore: fix path
sheremet-va Jul 11, 2023
61692fa
chore: docs and validation
sheremet-va Jul 11, 2023
b228f67
chore: provide env name in browser context
sheremet-va Jul 11, 2023
a2e30f7
chore: improve compatibility with node.js
sheremet-va Jul 11, 2023
87d46ec
chore: ignore isBuiltin
sheremet-va Jul 11, 2023
71b90fb
fix: correctly inline styles in VM
sheremet-va Jul 11, 2023
fcbfc26
chore: add Buffer to global
sheremet-va Jul 11, 2023
d2ff891
chore: always use native import.meta.resolve
sheremet-va Jul 11, 2023
6b1fdc6
chore: use the longest extension
sheremet-va Jul 11, 2023
3300459
fix: use the same primitives
sheremet-va Jul 11, 2023
6cf97ed
fix: support import.meta.vitest in vm pool
sheremet-va Jul 11, 2023
296a29f
chore: fix structure clone and buffer inconsistencies
sheremet-va Jul 11, 2023
1def1f9
chore: fix ts types
sheremet-va Jul 11, 2023
78459c7
chore: update svelte testing-library
sheremet-va Jul 11, 2023
709a5ed
chore: error is not the same
sheremet-va Jul 11, 2023
17fffdb
chore: skip single thread in vm
sheremet-va Jul 11, 2023
3bc2d0a
chore: move findNearestPackageData
sheremet-va Jul 12, 2023
9a3c310
chore: cleanup and docs
sheremet-va Jul 12, 2023
0989e27
chore: use correct pnpm
sheremet-va Jul 12, 2023
4504d02
chore: revert lockfile
sheremet-va Jul 12, 2023
9ec9fbe
chore: cleanup
sheremet-va Jul 12, 2023
46ef72b
chore: cleanup
sheremet-va Jul 19, 2023
553663a
Apply suggestions from code review
sheremet-va Jul 19, 2023
4ccbd5c
Update docs/config/index.md
sheremet-va Jul 19, 2023
7c045b4
chore: ignore version
sheremet-va Jul 19, 2023
dfed0c3
chore: cleanup
sheremet-va Jul 19, 2023
f3db7d6
chore: cleanup
sheremet-va Jul 28, 2023
3342ea4
fix: normalize cache path
sheremet-va Jul 28, 2023
213e7c6
Apply suggestions from code review
sheremet-va Jul 28, 2023
72f8735
chore: move updatestyle/removestyle
sheremet-va Jul 30, 2023
4e083b9
chore: lockfile
sheremet-va Jul 30, 2023
87abda7
chore: cleanup
sheremet-va Jul 31, 2023
8ec12e3
chore: web-worker depends on vitest 0.34.0
sheremet-va Jul 31, 2023
cca3e56
chore: cleanup
sheremet-va Jul 31, 2023
37b970d
chore: overwrite wrap
sheremet-va Jul 31, 2023
eaa8ac6
chore: primitives
sheremet-va Jul 31, 2023
2b2a73e
chore: have a cache for fs
sheremet-va Jul 31, 2023
36dbf7f
chore: cleanup
sheremet-va Jul 31, 2023
315bbab
chore: identifier->fileUrl
sheremet-va Jul 31, 2023
f6c5aee
chore: support data uri
sheremet-va Jul 31, 2023
68b2db9
chore: allow import .json, throw error on importing .css
sheremet-va Jul 31, 2023
710e291
chore: typo
sheremet-va Jul 31, 2023
82ae1a6
chore: types
sheremet-va Jul 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -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

Expand Down
74 changes: 72 additions & 2 deletions docs/config/index.md
Expand Up @@ -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

Expand Down Expand Up @@ -421,6 +425,7 @@ import type { Environment } from 'vitest'

export default <Environment>{
name: 'custom',
transformMode: 'ssr',
setup() {
// custom setup
return {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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<NonProjectOption />

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.
Expand Down
5 changes: 4 additions & 1 deletion docs/guide/cli.md
Expand Up @@ -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 <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 <name>` | Select reporter: `default`, `verbose`, `dot`, `junit`, `json`, or a path to a custom reporter |
Expand Down
20 changes: 19 additions & 1 deletion docs/guide/environment.md
Expand Up @@ -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 <Environment>{
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 {
Expand All @@ -45,6 +59,10 @@ export default <Environment>{
}
```

::: 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
Expand Down
6 changes: 5 additions & 1 deletion examples/mocks/vite.config.ts
Expand Up @@ -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'],
},
Expand Down
2 changes: 1 addition & 1 deletion 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[`<Hello /> > renders 1`] = `"<div>4 x 2 = 8</div><button>x1</button>"`;

Expand Down
2 changes: 1 addition & 1 deletion examples/svelte/package.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions 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 {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/client/main.ts
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/runner/src/types/runner.ts
Expand Up @@ -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<string, never>

export interface VitestRunner {
/**
Expand Down
5 changes: 5 additions & 0 deletions packages/vite-node/package.json
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/vite-node/rollup.config.js
Expand Up @@ -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',
}
Expand All @@ -29,6 +30,7 @@ const external = [
'vite/types/hot',
'node:url',
'node:events',
'node:vm',
]

const plugins = [
Expand Down
58 changes: 25 additions & 33 deletions packages/vite-node/src/client.ts
Expand Up @@ -17,7 +17,7 @@ const debugNative = createDebug('vite-node:client:native')

const clientStub = {
injectQuery: (id: string) => id,
createHotContext() {
createHotContext: () => {
return {
accept: () => {},
prune: () => {},
Expand All @@ -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<string, unknown> = {
export const DEFAULT_REQUEST_STUBS: Record<string, Record<string, unknown>> = {
'/@vite/client': clientStub,
'@vite/client': clientStub,
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 `<UNIT>:/` on windows: see nodejs/node#31710
const href = pathToFileURL(modulePath).href
Expand Down Expand Up @@ -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<string, any>, 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<string, any>) {
Expand All @@ -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
Expand Down