Skip to content

Commit af8de36

Browse files
authored
feat: add an option to enable Vite optimizer (#2912)
1 parent 610b1d4 commit af8de36

File tree

17 files changed

+178
-61
lines changed

17 files changed

+178
-61
lines changed

docs/config/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ Files to exclude from the test run, using glob pattern.
9191

9292
Handling for dependencies resolution.
9393

94+
#### deps.experimentalOptimizer
95+
96+
- **Type:** `DepOptimizationConfig & { enabled: boolean }`
97+
- **Version:** Vitets 0.29.0
98+
- **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html)
99+
100+
Enable dependency optimization. If you have a lot of tests, this might improve their performance.
101+
102+
For `jsdom` and `happy-dom` environments, when Vitest will encounter the external library, it will be bundled into a single file using esbuild and imported as a whole module. This is good for several reasons:
103+
104+
- Importing packages with a lot of imports is expensive. By bundling them into one file we can save a lot of time
105+
- Importing UI libraries is expensive because they are not meant to run inside Node.js
106+
- Your `alias` configuration is now respected inside bundled packages
107+
108+
You can opt-out of this behavior for certain packages with `exclude` option. You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs.
109+
110+
This options also inherits your `optimizeDeps` configuration. If you redefine `include`/`exclude`/`entries` option in `deps.experimentalOptimizer` it will overwrite your `optimizeDeps` when running tests.
111+
112+
:::note
113+
You will not be able to edit your `node_modules` code for debugging, since the code is actually located in your `cacheDir` or `test.cache.dir` directory. If you want to debug with `console.log` statements, edit it directly or force rebundling with `deps.experimentalOptimizer.force` option.
114+
:::
115+
94116
#### deps.external
95117

96118
- **Type:** `(string | RegExp)[]`

packages/vite-node/src/client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,18 @@ export class ViteNodeRunner {
204204
return !isInternalRequest(id) && !isNodeBuiltin(id)
205205
}
206206

207-
private async _resolveUrl(id: string, importee?: string): Promise<[url: string, fsPath: string]> {
207+
private async _resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> {
208208
// we don't pass down importee here, because otherwise Vite doesn't resolve it correctly
209209
// should be checked before normalization, because it removes this prefix
210-
if (importee && id.startsWith(VALID_ID_PREFIX))
211-
importee = undefined
210+
if (importer && id.startsWith(VALID_ID_PREFIX))
211+
importer = undefined
212212
id = normalizeRequestId(id, this.options.base)
213213
if (!this.shouldResolveId(id))
214214
return [id, id]
215215
const { path, exists } = toFilePath(id, this.root)
216216
if (!this.options.resolveId || exists)
217217
return [id, path]
218-
const resolved = await this.options.resolveId(id, importee)
218+
const resolved = await this.options.resolveId(id, importer)
219219
const resolvedId = resolved
220220
? normalizeRequestId(resolved.id, this.options.base)
221221
: id

packages/vite-node/src/externalize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ async function _shouldExternalize(
105105

106106
id = patchWindowsImportPath(id)
107107

108+
// always externalize Vite deps, they are too big to inline
109+
if (options?.cacheDir && id.includes(options.cacheDir))
110+
return id
111+
108112
if (matchExternalizePattern(id, options?.inline))
109113
return false
110114
if (matchExternalizePattern(id, options?.external))

packages/vite-node/src/server.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { performance } from 'node:perf_hooks'
2-
import { resolve } from 'pathe'
2+
import { existsSync } from 'node:fs'
3+
import { join, relative, resolve } from 'pathe'
34
import type { TransformResult, ViteDevServer } from 'vite'
45
import createDebug from 'debug'
56
import type { EncodedSourceMap } from '@jridgewell/trace-mapping'
@@ -17,6 +18,8 @@ export class ViteNodeServer {
1718
private fetchPromiseMap = new Map<string, Promise<FetchResult>>()
1819
private transformPromiseMap = new Map<string, Promise<TransformResult | null | undefined>>()
1920

21+
private existingOptimizedDeps = new Set<string>()
22+
2023
fetchCache = new Map<string, {
2124
duration?: number
2225
timestamp: number
@@ -34,9 +37,12 @@ export class ViteNodeServer {
3437
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
3538
// @ts-ignore ssr is not typed in Vite 2, but defined in Vite 3, so we can't use expect-error
3639
const ssrOptions = server.config.ssr
37-
if (ssrOptions) {
38-
options.deps ??= {}
3940

41+
options.deps ??= {}
42+
43+
options.deps.cacheDir = relative(server.config.root, server.config.cacheDir)
44+
45+
if (ssrOptions) {
4046
// we don't externalize ssr, because it has different semantics in Vite
4147
// if (ssrOptions.external) {
4248
// options.deps.external ??= []
@@ -65,10 +71,26 @@ export class ViteNodeServer {
6571
return shouldExternalize(id, this.options.deps, this.externalizeCache)
6672
}
6773

68-
async resolveId(id: string, importer?: string): Promise<ViteNodeResolveId | null> {
74+
private async ensureExists(id: string): Promise<boolean> {
75+
if (this.existingOptimizedDeps.has(id))
76+
return true
77+
if (existsSync(id)) {
78+
this.existingOptimizedDeps.add(id)
79+
return true
80+
}
81+
return new Promise<boolean>((resolve) => {
82+
setTimeout(() => {
83+
this.ensureExists(id).then(() => {
84+
resolve(true)
85+
})
86+
})
87+
})
88+
}
89+
90+
async resolveId(id: string, importer?: string, transformMode?: 'web' | 'ssr'): Promise<ViteNodeResolveId | null> {
6991
if (importer && !importer.startsWith(this.server.config.root))
7092
importer = resolve(this.server.config.root, importer)
71-
const mode = (importer && this.getTransformMode(importer)) || 'ssr'
93+
const mode = transformMode ?? ((importer && this.getTransformMode(importer)) || 'ssr')
7294
return this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' })
7395
}
7496

@@ -80,12 +102,12 @@ export class ViteNodeServer {
80102
return (ssrTransformResult?.map || null) as unknown as EncodedSourceMap | null
81103
}
82104

83-
async fetchModule(id: string): Promise<FetchResult> {
105+
async fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise<FetchResult> {
84106
id = normalizeModuleId(id)
85107
// reuse transform for concurrent requests
86108
if (!this.fetchPromiseMap.has(id)) {
87109
this.fetchPromiseMap.set(id,
88-
this._fetchModule(id)
110+
this._fetchModule(id, transformMode)
89111
.then((r) => {
90112
return this.options.sourcemap !== true ? { ...r, map: undefined } : r
91113
})
@@ -123,9 +145,20 @@ export class ViteNodeServer {
123145
return 'web'
124146
}
125147

126-
private async _fetchModule(id: string): Promise<FetchResult> {
148+
private async _fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise<FetchResult> {
127149
let result: FetchResult
128150

151+
const cacheDir = this.options.deps?.cacheDir
152+
153+
if (cacheDir && id.includes(cacheDir) && !id.includes(this.server.config.root)) {
154+
id = join(this.server.config.root, id)
155+
const timeout = setTimeout(() => {
156+
throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`)
157+
}, 5000) // CI can be quite slow
158+
await this.ensureExists(id)
159+
clearTimeout(timeout)
160+
}
161+
129162
const { path: filePath } = toFilePath(id, this.server.config.root)
130163

131164
const module = this.server.moduleGraph.getModuleById(id)
@@ -143,7 +176,7 @@ export class ViteNodeServer {
143176
}
144177
else {
145178
const start = performance.now()
146-
const r = await this._transformRequest(id)
179+
const r = await this._transformRequest(id, transformMode)
147180
duration = performance.now() - start
148181
result = { code: r?.code, map: r?.map as any }
149182
}
@@ -157,7 +190,7 @@ export class ViteNodeServer {
157190
return result
158191
}
159192

160-
private async _transformRequest(id: string) {
193+
private async _transformRequest(id: string, customTransformMode?: 'web' | 'ssr') {
161194
debugRequest(id)
162195

163196
let result: TransformResult | null = null
@@ -168,7 +201,9 @@ export class ViteNodeServer {
168201
return result
169202
}
170203

171-
if (this.getTransformMode(id) === 'web') {
204+
const transformMode = customTransformMode ?? this.getTransformMode(id)
205+
206+
if (transformMode === 'web') {
172207
// for components like Vue, we want to use the client side
173208
// plugins but then convert the code to be consumed by the server
174209
result = await this.server.transformRequest(id)

packages/vite-node/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type Arrayable<T> = T | Array<T>
88
export interface DepsHandlingOptions {
99
external?: (string | RegExp)[]
1010
inline?: (string | RegExp)[] | true
11+
cacheDir?: string
1112
/**
1213
* Try to guess the CJS version of a package when it's invalid ESM
1314
* @default false

packages/vitest/src/node/core.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,9 @@ export class Vitest {
155155
}
156156

157157
async typecheck(filters: string[] = []) {
158+
const { dir, root } = this.config
158159
const { include, exclude } = this.config.typecheck
159-
const testsFilesList = await this.globFiles(filters, include, exclude)
160+
const testsFilesList = this.filterFiles(await this.globFiles(include, exclude, dir || root), filters)
160161
const checker = new Typechecker(this, testsFilesList)
161162
this.typechecker = checker
162163
checker.onParseEnd(async ({ files, sourceErrors }) => {
@@ -606,32 +607,26 @@ export class Vitest {
606607
)))
607608
}
608609

609-
async globFiles(filters: string[], include: string[], exclude: string[]) {
610+
async globFiles(include: string[], exclude: string[], cwd: string) {
610611
const globOptions: fg.Options = {
611612
absolute: true,
612613
dot: true,
613-
cwd: this.config.dir || this.config.root,
614+
cwd,
614615
ignore: exclude,
615616
}
616617

617-
let testFiles = await fg(include, globOptions)
618-
619-
if (filters.length && process.platform === 'win32')
620-
filters = filters.map(f => toNamespacedPath(f))
621-
622-
if (filters.length)
623-
testFiles = testFiles.filter(i => filters.some(f => i.includes(f)))
624-
625-
return testFiles
618+
return fg(include, globOptions)
626619
}
627620

628-
async globTestFiles(filters: string[] = []) {
629-
const { include, exclude, includeSource } = this.config
621+
private _allTestsCache: string[] | null = null
622+
623+
async globAllTestFiles(config: ResolvedConfig, cwd: string) {
624+
const { include, exclude, includeSource } = config
630625

631-
const testFiles = await this.globFiles(filters, include, exclude)
626+
const testFiles = await this.globFiles(include, exclude, cwd)
632627

633628
if (includeSource) {
634-
const files = await this.globFiles(filters, includeSource, exclude)
629+
const files = await this.globFiles(includeSource, exclude, cwd)
635630

636631
await Promise.all(files.map(async (file) => {
637632
try {
@@ -645,9 +640,31 @@ export class Vitest {
645640
}))
646641
}
647642

643+
this._allTestsCache = testFiles
644+
645+
return testFiles
646+
}
647+
648+
filterFiles(testFiles: string[], filters: string[] = []) {
649+
if (filters.length && process.platform === 'win32')
650+
filters = filters.map(f => toNamespacedPath(f))
651+
652+
if (filters.length)
653+
return testFiles.filter(i => filters.some(f => i.includes(f)))
654+
648655
return testFiles
649656
}
650657

658+
async globTestFiles(filters: string[] = []) {
659+
const { dir, root } = this.config
660+
661+
const testFiles = this._allTestsCache ?? await this.globAllTestFiles(this.config, dir || root)
662+
663+
this._allTestsCache = null
664+
665+
return this.filterFiles(testFiles, filters)
666+
}
667+
651668
async isTargetFile(id: string, source?: string): Promise<boolean> {
652669
const relativeId = relative(this.config.dir || this.config.root, id)
653670
if (mm.isMatch(relativeId, this.config.exclude))

packages/vitest/src/node/create.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit
2727

2828
const server = await createServer(mergeConfig(config, mergeConfig(viteOverrides, { root: options.root })))
2929

30-
if (ctx.config.api?.port)
30+
// optimizer needs .listen() to be called
31+
if (ctx.config.api?.port || ctx.config.deps?.experimentalOptimizer?.enabled)
3132
await server.listen()
3233
else
3334
await server.pluginContainer.buildStart({})

packages/vitest/src/node/plugins/index.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
3333
options() {
3434
this.meta.watchMode = false
3535
},
36-
config(viteConfig: any) {
36+
async config(viteConfig: any) {
3737
// preliminary merge of options to be able to create server options for vite
3838
// however to allow vitest plugins to modify vitest config values
3939
// this is repeated in configResolved where the config is final
@@ -131,15 +131,37 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
131131
}
132132

133133
if (!options.browser) {
134-
// disable deps optimization
135-
Object.assign(config, {
136-
cacheDir: undefined,
137-
optimizeDeps: {
134+
const optimizeConfig: Partial<ViteConfig> = {}
135+
const optimizer = preOptions.deps?.experimentalOptimizer
136+
if (!optimizer?.enabled) {
137+
optimizeConfig.cacheDir = undefined
138+
optimizeConfig.optimizeDeps = {
138139
// experimental in Vite >2.9.2, entries remains to help with older versions
139140
disabled: true,
140141
entries: [],
141-
},
142-
})
142+
}
143+
}
144+
else {
145+
const entries = await ctx.globAllTestFiles(preOptions as ResolvedConfig, preOptions.dir || getRoot())
146+
optimizeConfig.cacheDir = preOptions.cache?.dir ?? 'node_modules/.vitest'
147+
optimizeConfig.optimizeDeps = {
148+
...viteConfig.optimizeDeps,
149+
...optimizer,
150+
disabled: false,
151+
entries: [...(optimizer.entries || viteConfig.optimizeDeps?.entries || []), ...entries],
152+
exclude: ['vitest', ...(optimizer.exclude || viteConfig.optimizeDeps?.exclude || [])],
153+
include: (optimizer.include || viteConfig.optimizeDeps?.include || []).filter((n: string) => n !== 'vitest'),
154+
}
155+
// Vite throws an error that it cannot rename "deps_temp", but optimization still works
156+
// let's not show this error to users
157+
const { error: logError } = console
158+
console.error = (...args) => {
159+
if (typeof args[0] === 'string' && args[0].includes('/deps_temp'))
160+
return
161+
return logError(...args)
162+
}
163+
}
164+
Object.assign(config, optimizeConfig)
143165
}
144166

145167
return config

packages/vitest/src/node/pool.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createBirpc } from 'birpc'
88
import type { RawSourceMap } from 'vite-node'
99
import type { ResolvedConfig, WorkerContext, WorkerRPC, WorkerTestEnvironment } from '../types'
1010
import { distDir, rootDir } from '../constants'
11-
import { AggregateError, groupBy } from '../utils'
11+
import { AggregateError, getEnvironmentTransformMode, groupBy } from '../utils'
1212
import { envsOrder, groupFilesByEnv } from '../utils/test-helpers'
1313
import type { Vitest } from './core'
1414

@@ -195,11 +195,13 @@ function createChannel(ctx: Vitest) {
195195
const r = await ctx.vitenode.transformRequest(id)
196196
return r?.map as RawSourceMap | undefined
197197
},
198-
fetch(id) {
199-
return ctx.vitenode.fetchModule(id)
198+
fetch(id, environment) {
199+
const transformMode = getEnvironmentTransformMode(ctx.config, environment)
200+
return ctx.vitenode.fetchModule(id, transformMode)
200201
},
201-
resolveId(id, importer) {
202-
return ctx.vitenode.resolveId(id, importer)
202+
resolveId(id, importer, environment) {
203+
const transformMode = getEnvironmentTransformMode(ctx.config, environment)
204+
return ctx.vitenode.resolveId(id, importer, transformMode)
203205
},
204206
onPathsCollected(paths) {
205207
ctx.state.collectPaths(paths)

packages/vitest/src/runtime/execute.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export class VitestExecutor extends ViteNodeRunner {
4343
return environment === 'node' ? !isNodeBuiltin(id) : !id.startsWith('node:')
4444
}
4545

46-
async resolveUrl(id: string, importee?: string) {
47-
if (importee && importee.startsWith('mock:'))
48-
importee = importee.slice(5)
49-
return super.resolveUrl(id, importee)
46+
async resolveUrl(id: string, importer?: string) {
47+
if (importer && importer.startsWith('mock:'))
48+
importer = importer.slice(5)
49+
return super.resolveUrl(id, importer)
5050
}
5151

5252
async dependencyRequest(id: string, fsPath: string, callstack: string[]): Promise<any> {

0 commit comments

Comments
 (0)