Skip to content

Commit

Permalink
perf: don't inline Vitest entry (#2819)
Browse files Browse the repository at this point in the history
* perf: don't inline Vitest entry

* build: remove runners-chunk

* chore: import environment via executor

* chore: pass down path correct path on windows

* chore: remove "initializeSpyModule" since it is no longer required
  • Loading branch information
sheremet-va committed Feb 6, 2023
1 parent aa1aa4d commit 570c639
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 70 deletions.
2 changes: 0 additions & 2 deletions packages/vitest/rollup.config.js
Expand Up @@ -76,8 +76,6 @@ export default ({ watch }) => defineConfig([
let id = chunkInfo.facadeModuleId || Object.keys(chunkInfo.modules).find(i => !i.includes('node_modules') && (i.includes('src/') || i.includes('src\\')))
if (id) {
id = normalize(id)
if (id.includes('runtime/runners'))
return 'runners-chunk.js'
const parts = Array.from(
new Set(relative(process.cwd(), id).split(/\//g)
.map(i => i.replace(/\..*$/, ''))
Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/node/config.ts
Expand Up @@ -18,11 +18,11 @@ const extraInlineDeps = [
// Vite client
/vite\w*\/dist\/client\/env.mjs/,
// Vitest
/\/vitest\/dist\/(runners-chunk|entry)\.js/,
/\/vitest\/dist\/runners\.js/,
// yarn's .store folder
/vitest-virtual-\w+\/dist\/(runners-chunk|entry)\.js/,
/vitest-virtual-\w+\/dist\/runners\.js/,
// cnpm
/@vitest\/dist\/(runners-chunk|entry)\.js/,
/@vitest\/dist\/runners\.js/,
// Nuxt
'@nuxt/test-utils',
]
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/index.ts
Expand Up @@ -3,7 +3,7 @@ export { createVitest } from './create'
export { VitestPlugin } from './plugins'
export { startVitest } from './cli-api'

export { VitestRunner } from '../runtime/execute'
export { VitestExecutor } from '../runtime/execute'
export type { ExecuteOptions } from '../runtime/execute'

export type { TestSequencer, TestSequencerConstructor } from './sequencers/types'
Expand Down
25 changes: 14 additions & 11 deletions packages/vitest/src/runtime/entry.ts
Expand Up @@ -2,15 +2,16 @@ import { promises as fs } from 'node:fs'
import mm from 'micromatch'
import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner'
import { startTests } from '@vitest/runner'
import { resolve } from 'pathe'
import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from '../types'
import { getWorkerState, resetModules } from '../utils'
import { vi } from '../integrations/vi'
import { envs } from '../integrations/env'
import { takeCoverageInsideWorker } from '../integrations/coverage'
import { distDir } from '../constants'
import { setupGlobalEnv, withEnv } from './setup.node'
import { VitestTestRunner } from './runners/test'
import { NodeBenchmarkRunner } from './runners/benchmark'
import { rpc } from './rpc'
import type { VitestExecutor } from './execute'

function groupBy<T, K extends string | number | symbol>(collection: T[], iteratee: (item: T) => K) {
return collection.reduce((acc, item) => {
Expand All @@ -21,17 +22,19 @@ function groupBy<T, K extends string | number | symbol>(collection: T[], iterate
}, {} as Record<K, T[]>)
}

async function getTestRunnerConstructor(config: ResolvedConfig): Promise<VitestRunnerConstructor> {
if (!config.runner)
return (config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner) as any as VitestRunnerConstructor
const mod = await import(config.runner)
async function getTestRunnerConstructor(config: ResolvedConfig, executor: VitestExecutor): Promise<VitestRunnerConstructor> {
if (!config.runner) {
const { VitestTestRunner, NodeBenchmarkRunner } = await executor.executeFile(resolve(distDir, 'runners.js'))
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): Promise<VitestRunner> {
const TestRunner = await getTestRunnerConstructor(config)
async function getTestRunner(config: ResolvedConfig, executor: VitestExecutor): Promise<VitestRunner> {
const TestRunner = await getTestRunnerConstructor(config, executor)
const testRunner = new TestRunner(config)

if (!testRunner.config)
Expand Down Expand Up @@ -65,12 +68,12 @@ async function getTestRunner(config: ResolvedConfig): Promise<VitestRunner> {
}

// browser shouldn't call this!
export async function run(files: string[], config: ResolvedConfig): Promise<void> {
export async function run(files: string[], config: ResolvedConfig, executor: VitestExecutor): Promise<void> {
await setupGlobalEnv(config)

const workerState = getWorkerState()

const runner = await getTestRunner(config)
const runner = await getTestRunner(config, executor)

// if calling from a worker, there will always be one file
// if calling with no-threads, this will be the whole suite
Expand Down Expand Up @@ -123,7 +126,7 @@ export async function run(files: string[], config: ResolvedConfig): Promise<void
if (!files || !files.length)
continue

await withEnv(environment, files[0].envOptions || config.environmentOptions || {}, async () => {
await withEnv(environment, files[0].envOptions || config.environmentOptions || {}, executor, async () => {
for (const { file } of files) {
// it doesn't matter if running with --threads
// if running with --no-threads, we usually want to reset everything before running a test
Expand Down
13 changes: 4 additions & 9 deletions packages/vitest/src/runtime/execute.ts
Expand Up @@ -11,20 +11,15 @@ export interface ExecuteOptions extends ViteNodeRunnerOptions {
mockMap: MockMap
}

export async function executeInViteNode(options: ExecuteOptions & { files: string[] }) {
const runner = new VitestRunner(options)
export async function createVitestExecutor(options: ExecuteOptions) {
const runner = new VitestExecutor(options)

await runner.executeId('/@vite/env')
await runner.mocker.initializeSpyModule()

const result: any[] = []
for (const file of options.files)
result.push(await runner.executeFile(file))

return result
return runner
}

export class VitestRunner extends ViteNodeRunner {
export class VitestExecutor extends ViteNodeRunner {
public mocker: VitestMocker

constructor(public options: ExecuteOptions) {
Expand Down
42 changes: 13 additions & 29 deletions packages/vitest/src/runtime/mocker.ts
Expand Up @@ -4,9 +4,9 @@ import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe'
import { getColors, getType } from '@vitest/utils'
import { getWorkerState } from '../utils/global'
import { getAllMockableProperties } from '../utils/base'
import { distDir } from '../constants'
import { spyOn } from '../integrations/spy'
import type { MockFactory, PendingSuiteMock } from '../types/mocker'
import type { VitestRunner } from './execute'
import type { VitestExecutor } from './execute'

class RefTracker {
private idMap = new Map<any, number>()
Expand Down Expand Up @@ -38,28 +38,26 @@ function isSpecialProp(prop: Key, parentType: string) {

export class VitestMocker {
private static pendingIds: PendingSuiteMock[] = []
private static spyModulePath = resolve(distDir, 'spy.js')
private static spyModule?: typeof import('../integrations/spy')
private resolveCache = new Map<string, Record<string, string>>()

constructor(
public runner: VitestRunner,
public executor: VitestExecutor,
) {}

private get root() {
return this.runner.options.root
return this.executor.options.root
}

private get base() {
return this.runner.options.base
return this.executor.options.base
}

private get mockMap() {
return this.runner.options.mockMap
return this.executor.options.mockMap
}

private get moduleCache() {
return this.runner.moduleCache
return this.executor.moduleCache
}

public getSuiteFilepath(): string {
Expand All @@ -78,7 +76,7 @@ export class VitestMocker {
}

private async resolvePath(rawId: string, importer: string) {
const [id, fsPath] = await this.runner.resolveUrl(rawId, importer)
const [id, fsPath] = await this.executor.resolveUrl(rawId, importer)
// external is node_module or unresolved module
// for example, some people mock "vscode" and don't have it installed
const external = !isAbsolute(fsPath) || fsPath.includes('/node_modules/') ? rawId : null
Expand Down Expand Up @@ -202,14 +200,6 @@ export class VitestMocker {
}

public mockObject(object: Record<Key, any>, mockExports: Record<Key, any> = {}) {
if (!VitestMocker.spyModule) {
throw new Error(
'Error: Spy module is not defined. '
+ 'This is likely an internal bug in Vitest. '
+ 'Please report it to https://github.com/vitest-dev/vitest/issues')
}
const spyModule = VitestMocker.spyModule

const finalizers = new Array<() => void>()
const refs = new RefTracker()

Expand Down Expand Up @@ -271,7 +261,7 @@ export class VitestMocker {
continue

if (isFunction) {
spyModule.spyOn(newContainer, property).mockImplementation(() => undefined)
spyOn(newContainer, property).mockImplementation(() => undefined)
// tinyspy retains length, but jest doesn't.
Object.defineProperty(newContainer[property], 'length', { value: 0 })
}
Expand Down Expand Up @@ -321,7 +311,7 @@ export class VitestMocker {

public async importActual<T>(rawId: string, importee: string): Promise<T> {
const { id, fsPath } = await this.resolvePath(rawId, importee)
const result = await this.runner.cachedRequest(id, fsPath, [importee])
const result = await this.executor.cachedRequest(id, fsPath, [importee])
return result as T
}

Expand All @@ -335,19 +325,13 @@ export class VitestMocker {
mock = this.resolveMockPath(fsPath, external)

if (mock === null) {
const mod = await this.runner.cachedRequest(id, fsPath, [importee])
const mod = await this.executor.cachedRequest(id, fsPath, [importee])
return this.mockObject(mod)
}

if (typeof mock === 'function')
return this.callFunctionMock(fsPath, mock)
return this.runner.dependencyRequest(mock, mock, [importee])
}

public async initializeSpyModule() {
if (VitestMocker.spyModule)
return
VitestMocker.spyModule = await this.runner.executeId(VitestMocker.spyModulePath)
return this.executor.dependencyRequest(mock, mock, [importee])
}

public async requestWithMock(url: string, callstack: string[]) {
Expand All @@ -367,7 +351,7 @@ export class VitestMocker {
const exports = {}
// Assign the empty exports object early to allow for cycles to work. The object will be filled by mockObject()
this.moduleCache.set(mockPath, { exports })
const mod = await this.runner.directRequest(url, url, callstack)
const mod = await this.executor.directRequest(url, url, callstack)
this.mockObject(mod, exports)
return exports
}
Expand Down
8 changes: 5 additions & 3 deletions packages/vitest/src/runtime/setup.node.ts
Expand Up @@ -12,6 +12,7 @@ import { setupSnapshotEnvironment } from '../integrations/snapshot/env'
import { NodeSnapshotEnvironment } from '../integrations/snapshot/environments/node'
import { rpc } from './rpc'
import { setupCommonEnv } from './setup.common'
import type { VitestExecutor } from './execute'

// this should only be used in Node
let globalSetup = false
Expand Down Expand Up @@ -154,8 +155,8 @@ export async function setupConsoleLogSpy() {
})
}

async function loadEnvironment(name: string) {
const pkg = await import(`vitest-environment-${name}`)
async function loadEnvironment(name: string, executor: VitestExecutor) {
const pkg = await executor.executeId(`vitest-environment-${name}`)
if (!pkg || !pkg.default || typeof pkg.default !== 'object' || typeof pkg.default.setup !== 'function') {
throw new Error(
`Environment "${name}" is not a valid environment. `
Expand All @@ -168,9 +169,10 @@ async function loadEnvironment(name: string) {
export async function withEnv(
name: ResolvedConfig['environment'],
options: ResolvedConfig['environmentOptions'],
executor: VitestExecutor,
fn: () => Promise<void>,
) {
const config: Environment = (environments as any)[name] || await loadEnvironment(name)
const config: Environment = (environments as any)[name] || await loadEnvironment(name, executor)
// @ts-expect-error untyped global
globalThis.__vitest_environment__ = config.name || name
expect.setState({
Expand Down
22 changes: 12 additions & 10 deletions packages/vitest/src/runtime/worker.ts
@@ -1,3 +1,4 @@
import { pathToFileURL } from 'node:url'
import { relative, resolve } from 'pathe'
import { createBirpc } from 'birpc'
import { workerId as poolId } from 'tinypool'
Expand All @@ -8,11 +9,13 @@ import type { ResolvedConfig, WorkerContext, WorkerRPC } from '../types'
import { distDir } from '../constants'
import { getWorkerState } from '../utils/global'
import type { MockMap } from '../types/mocker'
import { executeInViteNode } from './execute'
import type { VitestExecutor } from './execute'
import { createVitestExecutor } from './execute'
import { rpc } from './rpc'

let _viteNode: {
run: (files: string[], config: ResolvedConfig) => Promise<void>
run: (files: string[], config: ResolvedConfig, executor: VitestExecutor) => Promise<void>
executor: VitestExecutor
}

const moduleCache = new ModuleCacheMap()
Expand Down Expand Up @@ -45,10 +48,7 @@ async function startViteNode(ctx: WorkerContext) {
process.on('uncaughtException', e => catchError(e, 'Uncaught Exception'))
process.on('unhandledRejection', e => catchError(e, 'Unhandled Rejection'))

const { run } = (await executeInViteNode({
files: [
resolve(distDir, 'entry.js'),
],
const executor = await createVitestExecutor({
fetchModule(id) {
return rpc().fetch(id)
},
Expand All @@ -60,9 +60,11 @@ async function startViteNode(ctx: WorkerContext) {
interopDefault: config.deps.interopDefault,
root: config.root,
base: config.base,
}))[0]
})

_viteNode = { run }
const { run } = await import(pathToFileURL(resolve(distDir, 'entry.js')).href)

_viteNode = { run, executor }

return _viteNode
}
Expand Down Expand Up @@ -106,6 +108,6 @@ function init(ctx: WorkerContext) {

export async function run(ctx: WorkerContext) {
init(ctx)
const { run } = await startViteNode(ctx)
return run(ctx.files, ctx.config)
const { run, executor } = await startViteNode(ctx)
return run(ctx.files, ctx.config, executor)
}
4 changes: 2 additions & 2 deletions packages/web-worker/src/runner.ts
@@ -1,6 +1,6 @@
import { VitestRunner } from 'vitest/node'
import { VitestExecutor } from 'vitest/node'

export class InlineWorkerRunner extends VitestRunner {
export class InlineWorkerRunner extends VitestExecutor {
constructor(options: any, private context: any) {
super(options)
}
Expand Down

0 comments on commit 570c639

Please sign in to comment.