From f71dc0735eca8c460143c3bce83ae3cf198bce74 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 10:53:24 +0100 Subject: [PATCH 1/9] feat: add an option to enable Vite optimizer --- packages/vite-node/src/externalize.ts | 4 +++ packages/vite-node/src/server.ts | 43 +++++++++++++++++++---- packages/vitest/src/node/create.ts | 3 +- packages/vitest/src/node/plugins/index.ts | 19 ++++++---- packages/vitest/src/node/pool.ts | 5 +-- packages/vitest/src/runtime/worker.ts | 2 +- packages/vitest/src/types/config.ts | 4 +++ packages/vitest/src/types/worker.ts | 4 +-- 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/packages/vite-node/src/externalize.ts b/packages/vite-node/src/externalize.ts index b388593a5719..5d849cfddfe7 100644 --- a/packages/vite-node/src/externalize.ts +++ b/packages/vite-node/src/externalize.ts @@ -105,6 +105,10 @@ async function _shouldExternalize( id = patchWindowsImportPath(id) + // always externalize Vite deps, they are too big to inline + if (id.includes('.vite/deps')) + return id + if (matchExternalizePattern(id, options?.inline)) return false if (matchExternalizePattern(id, options?.external)) diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 434665b56cc6..7e9763495aa2 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -1,5 +1,6 @@ import { performance } from 'node:perf_hooks' -import { resolve } from 'pathe' +import { existsSync } from 'node:fs' +import { join, resolve } from 'pathe' import type { TransformResult, ViteDevServer } from 'vite' import createDebug from 'debug' import type { DebuggerOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types' @@ -16,6 +17,8 @@ export class ViteNodeServer { private fetchPromiseMap = new Map>() private transformPromiseMap = new Map>() + private existingOptimizedDeps = new Set() + fetchCache = new Map { + if (this.existingOptimizedDeps.has(id)) + return true + if (existsSync(id)) + return true + return new Promise((resolve) => { + setTimeout(() => { + this.ensureExists(id).then(() => { + this.existingOptimizedDeps.add(id) + resolve(true) + }) + }) + }) + } + async resolveId(id: string, importer?: string): Promise { + if (id.includes('.vite/deps')) { + id = join(this.server.config.root, id) + const timeout = setTimeout(() => { + throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`) + }, 5000) // CI can be quite slow + await this.ensureExists(id) + clearTimeout(timeout) + return { id } + } if (importer && !importer.startsWith(this.server.config.root)) importer = resolve(this.server.config.root, importer) const mode = (importer && this.getTransformMode(importer)) || 'ssr' @@ -79,12 +106,12 @@ export class ViteNodeServer { return (ssrTransformResult?.map || null) as unknown as RawSourceMap | null } - async fetchModule(id: string): Promise { + async fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise { id = normalizeModuleId(id) // reuse transform for concurrent requests if (!this.fetchPromiseMap.has(id)) { this.fetchPromiseMap.set(id, - this._fetchModule(id) + this._fetchModule(id, transformMode) .then((r) => { return this.options.sourcemap !== true ? { ...r, map: undefined } : r }) @@ -122,7 +149,7 @@ export class ViteNodeServer { return 'web' } - private async _fetchModule(id: string): Promise { + private async _fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise { let result: FetchResult const { path: filePath } = toFilePath(id, this.server.config.root) @@ -142,7 +169,7 @@ export class ViteNodeServer { } else { const start = performance.now() - const r = await this._transformRequest(id) + const r = await this._transformRequest(id, transformMode) duration = performance.now() - start result = { code: r?.code, map: r?.map as unknown as RawSourceMap } } @@ -156,7 +183,7 @@ export class ViteNodeServer { return result } - private async _transformRequest(id: string) { + private async _transformRequest(id: string, customTransformMode?: 'web' | 'ssr') { debugRequest(id) let result: TransformResult | null = null @@ -167,7 +194,9 @@ export class ViteNodeServer { return result } - if (this.getTransformMode(id) === 'web') { + const transformMode = customTransformMode ?? this.getTransformMode(id) + + if (transformMode === 'web') { // for components like Vue, we want to use the client side // plugins but then convert the code to be consumed by the server result = await this.server.transformRequest(id) diff --git a/packages/vitest/src/node/create.ts b/packages/vitest/src/node/create.ts index 4d64cf1c1901..52a3c6eb3358 100644 --- a/packages/vitest/src/node/create.ts +++ b/packages/vitest/src/node/create.ts @@ -27,7 +27,8 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit const server = await createServer(mergeConfig(config, mergeConfig(viteOverrides, { root: options.root }))) - if (ctx.config.api?.port) + // optimizer needs .listen() to be called + if (ctx.config.api?.port || ctx.config.deps?.experimentalOptimizer) await server.listen() else await server.pluginContainer.buildStart({}) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index ff525f526fc5..43a82ce5f7b8 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -131,15 +131,22 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t } if (!options.browser) { - // disable deps optimization - Object.assign(config, { - cacheDir: undefined, - optimizeDeps: { + const optimizeConfig: Partial = {} + if (!preOptions.deps?.experimentalOptimizer) { + optimizeConfig.cacheDir = undefined + optimizeConfig.optimizeDeps = { // experimental in Vite >2.9.2, entries remains to help with older versions disabled: true, entries: [], - }, - }) + } + } + else { + optimizeConfig.optimizeDeps = { + disabled: false, + exclude: ['vitest'], + } + } + Object.assign(config, optimizeConfig) } return config diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index a91bf2f684ea..c2b0673e422e 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -195,8 +195,9 @@ function createChannel(ctx: Vitest) { const r = await ctx.vitenode.transformRequest(id) return r?.map as RawSourceMap | undefined }, - fetch(id) { - return ctx.vitenode.fetchModule(id) + fetch(id, environment) { + const transformMode = environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' + return ctx.vitenode.fetchModule(id, ctx.config.deps?.experimentalOptimizer ? transformMode : undefined) }, resolveId(id, importer) { return ctx.vitenode.resolveId(id, importer) diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 7da111c14c80..69340909deeb 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -50,7 +50,7 @@ async function startViteNode(ctx: WorkerContext) { const executor = await createVitestExecutor({ fetchModule(id) { - return rpc().fetch(id) + return rpc().fetch(id, ctx.environment.name) }, resolveId(id, importer) { return rpc().resolveId(id, importer) diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index d49e4e27d9b4..6d9f36e4f2d9 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -67,6 +67,10 @@ export interface InlineConfig { * Handling for dependencies inlining or externalizing */ deps?: { + /** + * Enable dependency optimization. This can improve the performance of your tests. + */ + experimentalOptimizer?: boolean /** * Externalize means that Vite will bypass the package to native Node. * diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index ec154c08b5bc..307afe4d680e 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -1,6 +1,6 @@ import type { MessagePort } from 'node:worker_threads' import type { File, TaskResultPack, Test } from '@vitest/runner' -import type { FetchFunction, ModuleCacheMap, RawSourceMap, ViteNodeResolveId } from 'vite-node' +import type { FetchResult, ModuleCacheMap, RawSourceMap, ViteNodeResolveId } from 'vite-node' import type { BirpcReturn } from 'birpc' import type { MockMap } from './mocker' import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from './config' @@ -28,7 +28,7 @@ export interface AfterSuiteRunMeta { } export interface WorkerRPC { - fetch: FetchFunction + fetch: (id: string, environment: VitestEnvironment) => Promise resolveId: ResolveIdFunction getSourceMap: (id: string, force?: boolean) => Promise From d5dcfa0fce3968b5fd07d653938e260bd71d24cf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 11:04:35 +0100 Subject: [PATCH 2/9] chore: fix type error --- packages/web-worker/src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-worker/src/utils.ts b/packages/web-worker/src/utils.ts index 1ed47dd90b53..f4df2e56a4e2 100644 --- a/packages/web-worker/src/utils.ts +++ b/packages/web-worker/src/utils.ts @@ -62,11 +62,11 @@ export function createMessageEvent(data: any, transferOrOptions: StructuredSeria } export function getRunnerOptions() { - const { config, rpc, mockMap, moduleCache } = getWorkerState() + const { config, ctx, rpc, mockMap, moduleCache } = getWorkerState() return { fetchModule(id: string) { - return rpc.fetch(id) + return rpc.fetch(id, ctx.environment.name) }, resolveId(id: string, importer?: string) { return rpc.resolveId(id, importer) From 65b3334431753cec39b8d5c18766e9ae84b84740 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 14:18:27 +0100 Subject: [PATCH 3/9] chore: cleanup --- packages/vite-node/src/client.ts | 8 ++++---- packages/vite-node/src/server.ts | 23 ++++++++++++----------- packages/vitest/src/node/core.ts | 13 +++++++------ packages/vitest/src/node/create.ts | 2 +- packages/vitest/src/node/plugins/index.ts | 19 ++++++++++++++++--- packages/vitest/src/runtime/execute.ts | 8 ++++---- packages/vitest/src/types/config.ts | 6 ++++-- tsconfig.json | 5 +++-- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index 0d721405f935..a835a7b6f76f 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -208,18 +208,18 @@ export class ViteNodeRunner { return !isInternalRequest(id) && !isNodeBuiltin(id) } - private async _resolveUrl(id: string, importee?: string): Promise<[url: string, fsPath: string]> { + private async _resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> { // we don't pass down importee here, because otherwise Vite doesn't resolve it correctly // should be checked before normalization, because it removes this prefix - if (importee && id.startsWith(VALID_ID_PREFIX)) - importee = undefined + if (importer && id.startsWith(VALID_ID_PREFIX)) + importer = undefined id = normalizeRequestId(id, this.options.base) if (!this.shouldResolveId(id)) return [id, id] const { path, exists } = toFilePath(id, this.root) if (!this.options.resolveId || exists) return [id, path] - const resolved = await this.options.resolveId(id, importee) + const resolved = await this.options.resolveId(id, importer) const resolvedId = resolved ? normalizeRequestId(resolved.id, this.options.base) : id diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 7e9763495aa2..6d29e5d5be9f 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -70,12 +70,13 @@ export class ViteNodeServer { private async ensureExists(id: string): Promise { if (this.existingOptimizedDeps.has(id)) return true - if (existsSync(id)) + if (existsSync(id)) { + this.existingOptimizedDeps.add(id) return true + } return new Promise((resolve) => { setTimeout(() => { this.ensureExists(id).then(() => { - this.existingOptimizedDeps.add(id) resolve(true) }) }) @@ -83,15 +84,6 @@ export class ViteNodeServer { } async resolveId(id: string, importer?: string): Promise { - if (id.includes('.vite/deps')) { - id = join(this.server.config.root, id) - const timeout = setTimeout(() => { - throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`) - }, 5000) // CI can be quite slow - await this.ensureExists(id) - clearTimeout(timeout) - return { id } - } if (importer && !importer.startsWith(this.server.config.root)) importer = resolve(this.server.config.root, importer) const mode = (importer && this.getTransformMode(importer)) || 'ssr' @@ -152,6 +144,15 @@ export class ViteNodeServer { private async _fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise { let result: FetchResult + if (id.includes('.vite/deps') && !id.includes(this.server.config.root)) { + id = join(this.server.config.root, id) + const timeout = setTimeout(() => { + throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`) + }, 5000) // CI can be quite slow + await this.ensureExists(id) + clearTimeout(timeout) + } + const { path: filePath } = toFilePath(id, this.server.config.root) const module = this.server.moduleGraph.getModuleById(id) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 2dd8e1e6f9f2..7237edf8c3e6 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -155,8 +155,9 @@ export class Vitest { } async typecheck(filters: string[] = []) { + const { dir, root } = this.config const { include, exclude } = this.config.typecheck - const testsFilesList = await this.globFiles(filters, include, exclude) + const testsFilesList = await this.globFiles(filters, include, exclude, dir || root) const checker = new Typechecker(this, testsFilesList) this.typechecker = checker checker.onParseEnd(async ({ files, sourceErrors }) => { @@ -606,11 +607,11 @@ export class Vitest { ))) } - async globFiles(filters: string[], include: string[], exclude: string[]) { + async globFiles(filters: string[], include: string[], exclude: string[], cwd: string) { const globOptions: fg.Options = { absolute: true, dot: true, - cwd: this.config.dir || this.config.root, + cwd, ignore: exclude, } @@ -626,12 +627,12 @@ export class Vitest { } async globTestFiles(filters: string[] = []) { - const { include, exclude, includeSource } = this.config + const { include, exclude, includeSource, dir, root } = this.config - const testFiles = await this.globFiles(filters, include, exclude) + const testFiles = await this.globFiles(filters, include, exclude, dir || root) if (includeSource) { - const files = await this.globFiles(filters, includeSource, exclude) + const files = await this.globFiles(filters, includeSource, exclude, dir || root) await Promise.all(files.map(async (file) => { try { diff --git a/packages/vitest/src/node/create.ts b/packages/vitest/src/node/create.ts index 52a3c6eb3358..e254af4456f3 100644 --- a/packages/vitest/src/node/create.ts +++ b/packages/vitest/src/node/create.ts @@ -28,7 +28,7 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit const server = await createServer(mergeConfig(config, mergeConfig(viteOverrides, { root: options.root }))) // optimizer needs .listen() to be called - if (ctx.config.api?.port || ctx.config.deps?.experimentalOptimizer) + if (ctx.config.api?.port || ctx.config.deps?.experimentalOptimizer?.enabled) await server.listen() else await server.pluginContainer.buildStart({}) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 43a82ce5f7b8..22b2ad5d958e 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -33,7 +33,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t options() { this.meta.watchMode = false }, - config(viteConfig: any) { + async config(viteConfig: any) { // preliminary merge of options to be able to create server options for vite // however to allow vitest plugins to modify vitest config values // this is repeated in configResolved where the config is final @@ -132,7 +132,8 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t if (!options.browser) { const optimizeConfig: Partial = {} - if (!preOptions.deps?.experimentalOptimizer) { + const optimizer = preOptions.deps?.experimentalOptimizer + if (!optimizer?.enabled) { optimizeConfig.cacheDir = undefined optimizeConfig.optimizeDeps = { // experimental in Vite >2.9.2, entries remains to help with older versions @@ -141,9 +142,21 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t } } else { + const entries = await ctx.globFiles([], preOptions.include || [], preOptions.exclude || [], preOptions.dir || getRoot()) optimizeConfig.optimizeDeps = { disabled: false, - exclude: ['vitest'], + ...optimizer, + entries: [...(optimizer.entries || []), ...entries], + exclude: ['vitest', ...(optimizer.exclude || [])], + include: (optimizer.include || []).filter((n: string) => n !== 'vitest'), + } + // Vite throws an error that it cannot rename "deps_temp", but optimization still works + // let's not show this error to users + const { error: logError } = console + console.error = (...args) => { + if (typeof args[0] === 'string' && args[0].includes('.vite/deps_temp')) + return + return logError(...args) } } Object.assign(config, optimizeConfig) diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index de85684fbeee..6f2e271a998d 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -43,10 +43,10 @@ export class VitestExecutor extends ViteNodeRunner { return environment === 'node' ? !isNodeBuiltin(id) : !id.startsWith('node:') } - async resolveUrl(id: string, importee?: string) { - if (importee && importee.startsWith('mock:')) - importee = importee.slice(5) - return super.resolveUrl(id, importee) + async resolveUrl(id: string, importer?: string) { + if (importer && importer.startsWith('mock:')) + importer = importer.slice(5) + return super.resolveUrl(id, importer) } async dependencyRequest(id: string, fsPath: string, callstack: string[]): Promise { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 6d9f36e4f2d9..c838b80de54d 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -1,4 +1,4 @@ -import type { AliasOptions, CommonServerOptions } from 'vite' +import type { AliasOptions, CommonServerOptions, DepOptimizationConfig } from 'vite' import type { PrettyFormatOptions } from 'pretty-format' import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' import type { BuiltinReporters } from '../node/reporters' @@ -70,7 +70,9 @@ export interface InlineConfig { /** * Enable dependency optimization. This can improve the performance of your tests. */ - experimentalOptimizer?: boolean + experimentalOptimizer?: Omit & { + enabled: boolean + } /** * Externalize means that Vite will bypass the package to native Node. * diff --git a/tsconfig.json b/tsconfig.json index ec9831421c10..54fbe9d3a339 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "vitest/node": ["./packages/vitest/src/node/index.ts"], "vitest/config": ["./packages/vitest/src/config.ts"], "vitest/browser": ["./packages/vitest/src/browser.ts"], + "vitest/runners": ["./packages/vitest/src/runners.ts"], "vite-node": ["./packages/vite-node/src/index.ts"], "vite-node/client": ["./packages/vite-node/src/client.ts"], "vite-node/server": ["./packages/vite-node/src/server.ts"], @@ -43,8 +44,8 @@ "exclude": [ "**/dist/**", "./packages/vitest/dist/**", - "./packages/vitest/*.d.ts", - "./packages/vitest/*.d.cts", + "./packages/*/*.d.ts", + "./packages/*/*.d.cts", "./packages/ui/client/**", "./examples/**/*.*", "./bench/**", From e30bb220d1fb22e0282b774189bb45e9977cd876 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 15:37:10 +0100 Subject: [PATCH 4/9] docs: add experimentalOptimizer docs --- docs/config/index.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/config/index.md b/docs/config/index.md index cb62351448a5..22f965a8daa4 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -91,6 +91,28 @@ Files to exclude from the test run, using glob pattern. Handling for dependencies resolution. +#### deps.experimentalOptimizer + +- **Type:** `DepOptimizationConfig & { enabled: boolean }` +- **Version:** Vitets 0.29.0 +- **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html) + +Enable dependency optimization. This can improve the performance of your tests. + +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: + +- Importing packages with a lot of imports is expensive. By bundling them into one file we can save a lot of time +- Importing UI libraries is expensive because they are not meant to run inside Node.js +- Your `alias` configuration is now respected inside bundled packages + +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. + +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. + +:::note +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. +::: + #### deps.external - **Type:** `(string | RegExp)[]` From b8e07a116fbcf4c785460e767aa97b160d7da233 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 16:21:52 +0100 Subject: [PATCH 5/9] chore: cleanup --- docs/config/index.md | 2 +- packages/vite-node/src/externalize.ts | 2 +- packages/vite-node/src/server.ts | 13 +++++-- packages/vite-node/src/types.ts | 1 + packages/vitest/src/node/core.ts | 46 +++++++++++++++-------- packages/vitest/src/node/plugins/index.ts | 14 ++++--- 6 files changed, 51 insertions(+), 27 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index 22f965a8daa4..0e5abfaf1d84 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -97,7 +97,7 @@ Handling for dependencies resolution. - **Version:** Vitets 0.29.0 - **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html) -Enable dependency optimization. This can improve the performance of your tests. +Enable dependency optimization. If you have a lot of tests, this might improve their performance. 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: diff --git a/packages/vite-node/src/externalize.ts b/packages/vite-node/src/externalize.ts index 5d849cfddfe7..a75682ef4eae 100644 --- a/packages/vite-node/src/externalize.ts +++ b/packages/vite-node/src/externalize.ts @@ -106,7 +106,7 @@ async function _shouldExternalize( id = patchWindowsImportPath(id) // always externalize Vite deps, they are too big to inline - if (id.includes('.vite/deps')) + if (options?.cacheDir && id.includes(options.cacheDir)) return id if (matchExternalizePattern(id, options?.inline)) diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 6d29e5d5be9f..68054416af25 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -1,6 +1,6 @@ import { performance } from 'node:perf_hooks' import { existsSync } from 'node:fs' -import { join, resolve } from 'pathe' +import { join, relative, resolve } from 'pathe' import type { TransformResult, ViteDevServer } from 'vite' import createDebug from 'debug' import type { DebuggerOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types' @@ -36,9 +36,12 @@ export class ViteNodeServer { // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error // @ts-ignore ssr is not typed in Vite 2, but defined in Vite 3, so we can't use expect-error const ssrOptions = server.config.ssr - if (ssrOptions) { - options.deps ??= {} + options.deps ??= {} + + options.deps.cacheDir = relative(server.config.root, server.config.cacheDir) + + if (ssrOptions) { // we don't externalize ssr, because it has different semantics in Vite // if (ssrOptions.external) { // options.deps.external ??= [] @@ -144,7 +147,9 @@ export class ViteNodeServer { private async _fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise { let result: FetchResult - if (id.includes('.vite/deps') && !id.includes(this.server.config.root)) { + const cacheDir = this.options.deps?.cacheDir + + if (cacheDir && id.includes(cacheDir) && !id.includes(this.server.config.root)) { id = join(this.server.config.root, id) const timeout = setTimeout(() => { throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`) diff --git a/packages/vite-node/src/types.ts b/packages/vite-node/src/types.ts index 58d327e673e3..75cfcc8196d9 100644 --- a/packages/vite-node/src/types.ts +++ b/packages/vite-node/src/types.ts @@ -7,6 +7,7 @@ export type Arrayable = T | Array export interface DepsHandlingOptions { external?: (string | RegExp)[] inline?: (string | RegExp)[] | true + cacheDir?: string /** * Try to guess the CJS version of a package when it's invalid ESM * @default false diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 7237edf8c3e6..07bad002025c 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -157,7 +157,7 @@ export class Vitest { async typecheck(filters: string[] = []) { const { dir, root } = this.config const { include, exclude } = this.config.typecheck - const testsFilesList = await this.globFiles(filters, include, exclude, dir || root) + const testsFilesList = this.filterFiles(await this.globFiles(include, exclude, dir || root), filters) const checker = new Typechecker(this, testsFilesList) this.typechecker = checker checker.onParseEnd(async ({ files, sourceErrors }) => { @@ -607,7 +607,7 @@ export class Vitest { ))) } - async globFiles(filters: string[], include: string[], exclude: string[], cwd: string) { + async globFiles(include: string[], exclude: string[], cwd: string) { const globOptions: fg.Options = { absolute: true, dot: true, @@ -615,24 +615,18 @@ export class Vitest { ignore: exclude, } - let testFiles = await fg(include, globOptions) - - if (filters.length && process.platform === 'win32') - filters = filters.map(f => toNamespacedPath(f)) - - if (filters.length) - testFiles = testFiles.filter(i => filters.some(f => i.includes(f))) - - return testFiles + return fg(include, globOptions) } - async globTestFiles(filters: string[] = []) { - const { include, exclude, includeSource, dir, root } = this.config + private _allTestsCache: string[] | null = [] + + async globAllTestFiles(config: ResolvedConfig, cwd: string) { + const { include, exclude, includeSource } = config - const testFiles = await this.globFiles(filters, include, exclude, dir || root) + const testFiles = await this.globFiles(include, exclude, cwd) if (includeSource) { - const files = await this.globFiles(filters, includeSource, exclude, dir || root) + const files = await this.globFiles(includeSource, exclude, cwd) await Promise.all(files.map(async (file) => { try { @@ -646,9 +640,31 @@ export class Vitest { })) } + this._allTestsCache = testFiles + return testFiles } + filterFiles(testFiles: string[], filters: string[] = []) { + if (filters.length && process.platform === 'win32') + filters = filters.map(f => toNamespacedPath(f)) + + if (filters.length) + return testFiles.filter(i => filters.some(f => i.includes(f))) + + return testFiles + } + + async globTestFiles(filters: string[] = []) { + const { dir, root } = this.config + + const testFiles = this._allTestsCache ?? await this.globAllTestFiles(this.config, dir || root) + + this._allTestsCache = null + + return this.filterFiles(testFiles, filters) + } + async isTargetFile(id: string, source?: string): Promise { const relativeId = relative(this.config.dir || this.config.root, id) if (mm.isMatch(relativeId, this.config.exclude)) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 22b2ad5d958e..db538e288051 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -142,19 +142,21 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t } } else { - const entries = await ctx.globFiles([], preOptions.include || [], preOptions.exclude || [], preOptions.dir || getRoot()) + const entries = await ctx.globTestFiles() + optimizeConfig.cacheDir = preOptions.cache?.dir ?? 'node_modules/.vitest' optimizeConfig.optimizeDeps = { - disabled: false, + ...viteConfig.optimizeDeps, ...optimizer, - entries: [...(optimizer.entries || []), ...entries], - exclude: ['vitest', ...(optimizer.exclude || [])], - include: (optimizer.include || []).filter((n: string) => n !== 'vitest'), + disabled: false, + entries: [...(optimizer.entries || viteConfig.optimizeDeps?.entries || []), ...entries], + exclude: ['vitest', ...(optimizer.exclude || viteConfig.optimizeDeps?.exclude || [])], + include: (optimizer.include || viteConfig.optimizeDeps?.include || []).filter((n: string) => n !== 'vitest'), } // Vite throws an error that it cannot rename "deps_temp", but optimization still works // let's not show this error to users const { error: logError } = console console.error = (...args) => { - if (typeof args[0] === 'string' && args[0].includes('.vite/deps_temp')) + if (typeof args[0] === 'string' && args[0].includes('/deps_temp')) return return logError(...args) } From c3099d318736452cccb674c47b1ae63de128cedb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 16:28:43 +0100 Subject: [PATCH 6/9] chore: cleanup --- packages/vitest/src/node/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 07bad002025c..03ed78760d6c 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -618,7 +618,7 @@ export class Vitest { return fg(include, globOptions) } - private _allTestsCache: string[] | null = [] + private _allTestsCache: string[] | null = null async globAllTestFiles(config: ResolvedConfig, cwd: string) { const { include, exclude, includeSource } = config From 134f6ed38213377f60004cf7a1cee6f5295564ea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 17:07:14 +0100 Subject: [PATCH 7/9] chore: use environment when resolving id --- packages/vite-node/src/server.ts | 4 ++-- packages/vitest/src/node/plugins/index.ts | 2 +- packages/vitest/src/node/pool.ts | 5 +++-- packages/vitest/src/runtime/worker.ts | 2 +- packages/vitest/src/types/worker.ts | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 68054416af25..6122dfce4397 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -86,10 +86,10 @@ export class ViteNodeServer { }) } - async resolveId(id: string, importer?: string): Promise { + async resolveId(id: string, importer?: string, transformMode?: 'web' | 'ssr'): Promise { if (importer && !importer.startsWith(this.server.config.root)) importer = resolve(this.server.config.root, importer) - const mode = (importer && this.getTransformMode(importer)) || 'ssr' + const mode = transformMode ?? ((importer && this.getTransformMode(importer)) || 'ssr') return this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' }) } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index db538e288051..15bf1f619a19 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -142,7 +142,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t } } else { - const entries = await ctx.globTestFiles() + const entries = await ctx.globAllTestFiles(preOptions as ResolvedConfig, preOptions.dir || getRoot()) optimizeConfig.cacheDir = preOptions.cache?.dir ?? 'node_modules/.vitest' optimizeConfig.optimizeDeps = { ...viteConfig.optimizeDeps, diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index c2b0673e422e..3e4f211ca74f 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -199,8 +199,9 @@ function createChannel(ctx: Vitest) { const transformMode = environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' return ctx.vitenode.fetchModule(id, ctx.config.deps?.experimentalOptimizer ? transformMode : undefined) }, - resolveId(id, importer) { - return ctx.vitenode.resolveId(id, importer) + resolveId(id, importer, environment) { + const transformMode = environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' + return ctx.vitenode.resolveId(id, importer, ctx.config.deps?.experimentalOptimizer ? transformMode : undefined) }, onPathsCollected(paths) { ctx.state.collectPaths(paths) diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 69340909deeb..53e2f731f41b 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -53,7 +53,7 @@ async function startViteNode(ctx: WorkerContext) { return rpc().fetch(id, ctx.environment.name) }, resolveId(id, importer) { - return rpc().resolveId(id, importer) + return rpc().resolveId(id, importer, ctx.environment.name) }, moduleCache, mockMap, diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 307afe4d680e..fd231ed43020 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -29,7 +29,7 @@ export interface AfterSuiteRunMeta { export interface WorkerRPC { fetch: (id: string, environment: VitestEnvironment) => Promise - resolveId: ResolveIdFunction + resolveId: (id: string, importer: string | undefined, environment: VitestEnvironment) => Promise getSourceMap: (id: string, force?: boolean) => Promise onFinished: (files: File[], errors?: unknown[]) => void From 9da36063fe7d8e2903ffbdb3cf6ddc096496611b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 17:13:48 +0100 Subject: [PATCH 8/9] chore: pass down environment name --- packages/vitest/src/runtime/loader.ts | 2 +- packages/web-worker/src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/runtime/loader.ts b/packages/vitest/src/runtime/loader.ts index 421b951a6a3d..12e501cb8087 100644 --- a/packages/vitest/src/runtime/loader.ts +++ b/packages/vitest/src/runtime/loader.ts @@ -50,7 +50,7 @@ export const resolve: Resolver = async (url, context, next) => { const id = normalizeModuleId(url) const importer = normalizeModuleId(parentURL) - const resolved = await resolver(id, importer) + const resolved = await resolver(id, importer, state.ctx.environment.name) let result: ResolveResult let filepath: string diff --git a/packages/web-worker/src/utils.ts b/packages/web-worker/src/utils.ts index f4df2e56a4e2..9104707e9651 100644 --- a/packages/web-worker/src/utils.ts +++ b/packages/web-worker/src/utils.ts @@ -69,7 +69,7 @@ export function getRunnerOptions() { return rpc.fetch(id, ctx.environment.name) }, resolveId(id: string, importer?: string) { - return rpc.resolveId(id, importer) + return rpc.resolveId(id, importer, ctx.environment.name) }, moduleCache, mockMap, From 9e8e15c8a32d2b60fe8ce69047b1a76044b430fa Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 Feb 2023 17:49:12 +0100 Subject: [PATCH 9/9] chore: cleanup --- packages/vitest/src/node/pool.ts | 10 +++++----- packages/vitest/src/utils/base.ts | 8 +++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index 3e4f211ca74f..fcc85794a403 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -8,7 +8,7 @@ import { createBirpc } from 'birpc' import type { RawSourceMap } from 'vite-node' import type { ResolvedConfig, WorkerContext, WorkerRPC, WorkerTestEnvironment } from '../types' import { distDir, rootDir } from '../constants' -import { AggregateError, groupBy } from '../utils' +import { AggregateError, getEnvironmentTransformMode, groupBy } from '../utils' import { envsOrder, groupFilesByEnv } from '../utils/test-helpers' import type { Vitest } from './core' @@ -196,12 +196,12 @@ function createChannel(ctx: Vitest) { return r?.map as RawSourceMap | undefined }, fetch(id, environment) { - const transformMode = environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' - return ctx.vitenode.fetchModule(id, ctx.config.deps?.experimentalOptimizer ? transformMode : undefined) + const transformMode = getEnvironmentTransformMode(ctx.config, environment) + return ctx.vitenode.fetchModule(id, transformMode) }, resolveId(id, importer, environment) { - const transformMode = environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' - return ctx.vitenode.resolveId(id, importer, ctx.config.deps?.experimentalOptimizer ? transformMode : undefined) + const transformMode = getEnvironmentTransformMode(ctx.config, environment) + return ctx.vitenode.resolveId(id, importer, transformMode) }, onPathsCollected(paths) { ctx.state.collectPaths(paths) diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index a8042d5b0d17..9bbe16341b9b 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -1,4 +1,4 @@ -import type { Arrayable, DeepMerge, Nullable } from '../types' +import type { Arrayable, DeepMerge, Nullable, ResolvedConfig, VitestEnvironment } from '../types' function isFinalObj(obj: any) { return obj === Object.prototype || obj === Function.prototype || obj === RegExp.prototype @@ -123,3 +123,9 @@ export function stdout(): NodeJS.WriteStream { // eslint-disable-next-line no-console return console._stdout || process.stdout } + +export function getEnvironmentTransformMode(config: ResolvedConfig, environment: VitestEnvironment) { + if (!config.deps?.experimentalOptimizer?.enabled) + return undefined + return environment === 'happy-dom' || environment === 'jsdom' ? 'web' : 'ssr' +}