From 570c639e4e2bd13e108c17e1391092907f5108f1 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 6 Feb 2023 18:56:32 +0100 Subject: [PATCH] perf: don't inline Vitest entry (#2819) * 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 --- packages/vitest/rollup.config.js | 2 -- packages/vitest/src/node/config.ts | 6 ++-- packages/vitest/src/node/index.ts | 2 +- packages/vitest/src/runtime/entry.ts | 25 ++++++++------ packages/vitest/src/runtime/execute.ts | 13 +++---- packages/vitest/src/runtime/mocker.ts | 42 +++++++---------------- packages/vitest/src/runtime/setup.node.ts | 8 +++-- packages/vitest/src/runtime/worker.ts | 22 ++++++------ packages/web-worker/src/runner.ts | 4 +-- 9 files changed, 54 insertions(+), 70 deletions(-) diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index d29c799a669b..eb650f92df6e 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -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(/\..*$/, '')) diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 5f02006a161d..5b67b53511df 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -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', ] diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index ebba28800a2b..804c24f70293 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -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' diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index e2c8591b9950..97cc6efd7061 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -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(collection: T[], iteratee: (item: T) => K) { return collection.reduce((acc, item) => { @@ -21,17 +22,19 @@ function groupBy(collection: T[], iterate }, {} as Record) } -async function getTestRunnerConstructor(config: ResolvedConfig): Promise { - 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 { + 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 { - const TestRunner = await getTestRunnerConstructor(config) +async function getTestRunner(config: ResolvedConfig, executor: VitestExecutor): Promise { + const TestRunner = await getTestRunnerConstructor(config, executor) const testRunner = new TestRunner(config) if (!testRunner.config) @@ -65,12 +68,12 @@ async function getTestRunner(config: ResolvedConfig): Promise { } // browser shouldn't call this! -export async function run(files: string[], config: ResolvedConfig): Promise { +export async function run(files: string[], config: ResolvedConfig, executor: VitestExecutor): Promise { 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 @@ -123,7 +126,7 @@ export async function run(files: string[], config: ResolvedConfig): Promise { + 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 diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index c300ccf14d54..de85684fbeee 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -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) { diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 6c6cc60a46b3..b98324fbde4c 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -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() @@ -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>() 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 { @@ -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 @@ -202,14 +200,6 @@ export class VitestMocker { } public mockObject(object: Record, mockExports: Record = {}) { - 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() @@ -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 }) } @@ -321,7 +311,7 @@ export class VitestMocker { public async importActual(rawId: string, importee: string): Promise { 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 } @@ -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[]) { @@ -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 } diff --git a/packages/vitest/src/runtime/setup.node.ts b/packages/vitest/src/runtime/setup.node.ts index 3deb9f22a46f..835fffa558a2 100644 --- a/packages/vitest/src/runtime/setup.node.ts +++ b/packages/vitest/src/runtime/setup.node.ts @@ -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 @@ -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. ` @@ -168,9 +169,10 @@ async function loadEnvironment(name: string) { export async function withEnv( name: ResolvedConfig['environment'], options: ResolvedConfig['environmentOptions'], + executor: VitestExecutor, fn: () => Promise, ) { - 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({ diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 8590fa55e26d..eb19e5e1564e 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/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' @@ -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 + run: (files: string[], config: ResolvedConfig, executor: VitestExecutor) => Promise + executor: VitestExecutor } const moduleCache = new ModuleCacheMap() @@ -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) }, @@ -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 } @@ -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) } diff --git a/packages/web-worker/src/runner.ts b/packages/web-worker/src/runner.ts index d7e7be3373a9..3f5b7266325a 100644 --- a/packages/web-worker/src/runner.ts +++ b/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) }