Visitar URL original
perf: reuse fsModuleCache when filtering by source (#11428) · vitest-dev/vitest@7ebd95f · GitHub
Skip to content

Commit 7ebd95f

Browse files
authored
perf: reuse fsModuleCache when filtering by source (#11428)
1 parent d6620fa commit 7ebd95f

9 files changed

Lines changed: 138 additions & 53 deletions

File tree

‎packages/vitest/src/node/ast-collect.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
createTaskName,
1717
} from '../utils/tasks'
1818
import { detectCodeBlock } from '../utils/test-helpers'
19-
import { toRollupError } from './environments/fetchModule'
19+
import { toRollupError } from './environments/transformService'
2020

2121
interface ParsedFile extends File {
2222
start: number

‎packages/vitest/src/node/core.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type { CancelReason, File } from '../runtime/runner/types'
77
import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general'
88
import type { SourceModuleDiagnostic, SourceModuleLocations } from '../types/module-locations'
99
import type { PluginHarness } from './config/pluginHarness'
10-
import type { VitestFetchFunction } from './environments/fetchModule'
1110
import type { Logger } from './logger'
1211
import type { VitestPackageInstaller } from './packageInstaller'
1312
import type { ProcessPool } from './pool'
@@ -41,8 +40,8 @@ import { VitestCache } from './cache'
4140
import { FileSystemModuleCache } from './cache/fsModuleCache'
4241
import { matchesProjectFilter, resolveConfig } from './config/resolveConfig'
4342
import { getCoverageProvider } from './coverage'
44-
import { createFetchModuleFunction } from './environments/fetchModule'
4543
import { ServerModuleRunner } from './environments/serverRunner'
44+
import { ModuleTransformService } from './environments/transformService'
4645
import { FilesNotFoundError } from './errors'
4746
import {
4847
collectModuleDurationsDiagnostic,
@@ -169,7 +168,7 @@ export class Vitest {
169168
/** @internal */ runner!: ModuleRunner
170169
/** @internal */ _testRun: TestRun
171170
/** @internal */ _resolver!: VitestResolver
172-
/** @internal */ _fetcher!: VitestFetchFunction
171+
/** @internal */ _transformService!: ModuleTransformService
173172
/** @internal */ _fsCache!: FileSystemModuleCache
174173
/** @internal */ _tmpDir: string | undefined = join(tmpdir(), nanoid())
175174
/** @internal */ _traces!: Traces
@@ -256,7 +255,7 @@ export class Vitest {
256255
this._resolver = new VitestResolver(this.viteConfig.cacheDir, resolved)
257256
// a closed run removes the temp dir, so a restart must allocate a new one
258257
const tmpDir = (this._tmpDir ??= join(tmpdir(), nanoid()))
259-
this._fetcher = createFetchModuleFunction(
258+
this._transformService = new ModuleTransformService(
260259
this._resolver,
261260
resolved,
262261
this._fsCache,
@@ -324,7 +323,7 @@ export class Vitest {
324323
this.runner =
325324
resolved.experimental.viteModuleRunner === false
326325
? new NativeModuleRunner(resolved.root)
327-
: new ServerModuleRunner(environment, this._fetcher, resolved)
326+
: new ServerModuleRunner(environment, this._transformService, resolved)
328327
this.vcs = await loadVCSProvider(this.runner, resolved.experimental.vcsProvider)
329328

330329
if (resolved.watch) {

‎packages/vitest/src/node/environments/serverRunner.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DevEnvironment, ViteDevServer } from 'vite'
22
import type { ResolvedConfig } from '../types/config'
3-
import type { VitestFetchFunction } from './fetchModule'
3+
import type { ModuleTransformService } from './transformService'
44
import { readFile } from 'node:fs/promises'
55
import { isRunnableDevEnvironment } from 'vite'
66
import { ModuleRunner } from 'vite/module-runner'
@@ -10,7 +10,7 @@ import { normalizeResolvedIdToUrl } from './normalizeUrl'
1010
export class ServerModuleRunner extends ModuleRunner {
1111
constructor(
1212
private environment: DevEnvironment,
13-
fetcher: VitestFetchFunction,
13+
transformService: ModuleTransformService,
1414
private config: ResolvedConfig,
1515
) {
1616
super(
@@ -29,7 +29,13 @@ export class ServerModuleRunner extends ModuleRunner {
2929
return { error: new Error(`Unknown method: ${name}. Expected "fetchModule".`) }
3030
}
3131
try {
32-
const result = await fetcher(data[0], data[1], environment, false, data[2])
32+
const result = await transformService.fetch(
33+
data[0],
34+
data[1],
35+
environment,
36+
false,
37+
data[2],
38+
)
3339
if ('tmp' in result) {
3440
const code = await readFile(result.tmp)
3541
return { result: { ...result, code } }
@@ -61,14 +67,14 @@ export class ServerModuleRunner extends ModuleRunner {
6167
// call `server.environments.ssr.runner.import(...)` get Vitest's module runner.
6268
export function installSsrModuleRunner(
6369
server: ViteDevServer,
64-
fetcher: VitestFetchFunction,
70+
transformService: ModuleTransformService,
6571
config: ResolvedConfig,
6672
): void {
6773
const ssrEnvironment = server.environments.ssr
6874
if (!isRunnableDevEnvironment(ssrEnvironment)) {
6975
return
7076
}
71-
const ssrRunner = new ServerModuleRunner(ssrEnvironment, fetcher, config)
77+
const ssrRunner = new ServerModuleRunner(ssrEnvironment, transformService, config)
7278
Object.defineProperty(ssrEnvironment, 'runner', {
7379
value: ssrRunner,
7480
writable: true,

packages/vitest/src/node/environments/fetchModule.ts renamed to packages/vitest/src/node/environments/transformService.ts

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const saveCachePromises = new Map<
4242
>()
4343
const readFilePromises = new Map<string, Promise<string | null>>()
4444

45-
class ModuleFetcher {
45+
export class ModuleTransformService {
4646
private tmpDirectories = new Set<string>()
4747
private fsCacheEnabled: boolean
4848
// the module type is only needed by the evaluator to decide if CJS
@@ -54,13 +54,48 @@ class ModuleFetcher {
5454
private resolver: VitestResolver,
5555
private config: ResolvedConfig,
5656
private fsCache: FileSystemModuleCache,
57+
private traces: Traces,
5758
private tmpProjectDir: string,
5859
) {
5960
this.fsCacheEnabled = config.fsModuleCache === true
6061
this.detectModuleType = config.injectCjsGlobals === false
6162
}
6263

6364
async fetch(
65+
url: string,
66+
importer: string | undefined,
67+
environment: DevEnvironment,
68+
makeTmpCopies?: boolean,
69+
options?: FetchFunctionOptions,
70+
otelCarrier?: OTELCarrier,
71+
): Promise<FetchResult | FetchCachedFileSystemResult> {
72+
await this.traces.waitInit()
73+
const context = otelCarrier ? this.traces.getContextFromCarrier(otelCarrier) : undefined
74+
return this.traces.$('vitest.module.transform', context ? { context } : {}, (span) =>
75+
this.fetchInSpan(span, url, importer, environment, makeTmpCopies, options),
76+
)
77+
}
78+
79+
/**
80+
* Transforms the module like `environment.transformRequest`, but reads and
81+
* populates the `fsModuleCache` when it is enabled.
82+
*/
83+
async transform(id: string, environment: DevEnvironment): Promise<TransformResult | null> {
84+
await this.traces.waitInit()
85+
const result = await this.traces.$('vitest.module.transform', (span) =>
86+
this.fetchInSpan(span, id, undefined, environment),
87+
)
88+
if ('id' in result) {
89+
const transformResult = environment.moduleGraph.getModuleById(result.id)?.transformResult
90+
if (transformResult) {
91+
return transformResult
92+
}
93+
}
94+
// externalized modules are never transformed by `fetch`
95+
return environment.transformRequest(id)
96+
}
97+
98+
private async fetchInSpan(
6499
trace: Span,
65100
url: string,
66101
importer: string | undefined,
@@ -531,34 +566,6 @@ function getSafeModulePaths(environment: DevEnvironment): Set<string> | undefine
531566
return config.safeModulePaths
532567
}
533568

534-
export interface VitestFetchFunction {
535-
(
536-
url: string,
537-
importer: string | undefined,
538-
environment: DevEnvironment,
539-
cacheFs?: boolean,
540-
options?: FetchFunctionOptions,
541-
otelCarrier?: OTELCarrier,
542-
): Promise<FetchResult | FetchCachedFileSystemResult>
543-
}
544-
545-
export function createFetchModuleFunction(
546-
resolver: VitestResolver,
547-
config: ResolvedConfig,
548-
fsCache: FileSystemModuleCache,
549-
traces: Traces,
550-
tmpProjectDir: string,
551-
): VitestFetchFunction {
552-
const fetcher = new ModuleFetcher(resolver, config, fsCache, tmpProjectDir)
553-
return async (url, importer, environment, cacheFs, options, otelCarrier) => {
554-
await traces.waitInit()
555-
const context = otelCarrier ? traces.getContextFromCarrier(otelCarrier) : undefined
556-
return traces.$('vitest.module.transform', context ? { context } : {}, (span) =>
557-
fetcher.fetch(span, url, importer, environment, cacheFs, options),
558-
)
559-
}
560-
}
561-
562569
let SOURCEMAPPING_URL = 'sourceMa'
563570
SOURCEMAPPING_URL += 'ppingURL'
564571

‎packages/vitest/src/node/plugins/ssrRunnerFixer.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function SsrRunnerFixerPlugin(harness: PluginHarness): Plugin {
1212
order: 'pre',
1313
handler(server) {
1414
const vitest = harness.getVitest()
15-
installSsrModuleRunner(server, vitest._fetcher, vitest.config)
15+
installSsrModuleRunner(server, vitest._transformService, vitest.config)
1616
},
1717
},
1818
}

‎packages/vitest/src/node/pools/rpc.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { existsSync, mkdirSync } from 'node:fs'
1010
import { fileURLToPath } from 'node:url'
1111
import { cleanUrl } from '@vitest/utils/helpers'
1212
import { isBuiltin, toBuiltin } from '../../utils/modules'
13-
import { handleRollupError } from '../environments/fetchModule'
1413
import { normalizeResolvedIdToUrl } from '../environments/normalizeUrl'
14+
import { handleRollupError } from '../environments/transformService'
1515

1616
interface MethodsOptions {
1717
cacheFs?: boolean
@@ -70,8 +70,8 @@ export function createMethodsRPC(
7070
const state = project.vitest.state
7171
const start = performance.now()
7272

73-
return await project
74-
._fetcher(url, importer, environment, cacheFs, options, otelCarrier)
73+
return await project._transformService
74+
.fetch(url, importer, environment, cacheFs, options, otelCarrier)
7575
.then((result) => {
7676
const metadata = state.metadata[project.name]
7777
if ('externalize' in result) {

‎packages/vitest/src/node/project.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { ModuleRunner } from 'vite/module-runner'
33
import type { Typechecker } from '../typecheck/typechecker'
44
import type { ProvidedContext } from '../types/general'
55
import type { OnTestsRerunHandler, Vitest } from './core'
6-
import type { VitestFetchFunction } from './environments/fetchModule'
76
import type { GlobalSetupFile } from './globalSetup'
87
import type { TestSpecificationOptions } from './test-specification'
98
import type { ParentProjectBrowser, ProjectBrowser } from './types/browser'
@@ -19,8 +18,8 @@ import { createDefinesScript } from '../utils/config-helpers'
1918
import { NativeModuleRunner } from '../utils/nativeModuleRunner'
2019
import { BenchmarkManager } from './benchmark'
2120
import { serializeConfig } from './config/serializeConfig'
22-
import { createFetchModuleFunction } from './environments/fetchModule'
2321
import { ServerModuleRunner } from './environments/serverRunner'
22+
import { ModuleTransformService } from './environments/transformService'
2423
import { loadGlobalSetupFiles } from './globalSetup'
2524
import { listenClusterServer } from './plugins/browserLoader'
2625
import { getFilePoolName } from './pool'
@@ -62,7 +61,7 @@ export class TestProject {
6261

6362
/** @internal */ typechecker?: Typechecker
6463
/** @internal */ _resolver!: VitestResolver
65-
/** @internal */ _fetcher!: VitestFetchFunction
64+
/** @internal */ _transformService!: ModuleTransformService
6665
/** @internal */ _serializedDefines?: string
6766
/** @internal */ _sharedViteServer = false
6867
/** @internal */ testFilesList: string[] | null = null
@@ -97,7 +96,7 @@ export class TestProject {
9796
_initializeRunners(server: ViteDevServer) {
9897
this._serializedDefines = createDefinesScript(this.config._scriptDefines)
9998
this._resolver = new VitestResolver(server.config.cacheDir, this.config)
100-
this._fetcher = createFetchModuleFunction(
99+
this._transformService = new ModuleTransformService(
101100
this._resolver,
102101
this.config,
103102
this.vitest._fsCache,
@@ -109,7 +108,7 @@ export class TestProject {
109108
this.runner =
110109
this.config.experimental.viteModuleRunner === false
111110
? new NativeModuleRunner(this.config.root)
112-
: new ServerModuleRunner(environment, this._fetcher, this.config)
111+
: new ServerModuleRunner(environment, this._transformService, this.config)
113112
}
114113

115114
// "provide" is a property, not a method to keep the context when destructed in the global setup,
@@ -533,14 +532,14 @@ export class TestProject {
533532
const project = new TestProject(vitest, vitest.vite, vitest.viteConfig, vitest.config)
534533
project.runner = vitest.runner
535534
project._resolver = vitest._resolver
536-
project._fetcher = vitest._fetcher
535+
project._transformService = vitest._transformService
537536
project._serializedDefines = createDefinesScript(vitest.config._scriptDefines)
538537
return project
539538
}
540539

541540
/**
542541
* Create a sibling project that shares server-derived resources (Vite server,
543-
* runner, resolver, fetcher) with a primary project. The sibling has its own
542+
* runner, resolver, transform service) with a primary project. The sibling has its own
544543
* distinct `projectConfig`, but the same `viteConfig` reference as the primary.
545544
*
546545
* Used for browser-instance and benchmark variants whose entries share a
@@ -552,7 +551,7 @@ export class TestProject {
552551
const sibling = new TestProject(parent.vitest, parent.vite, parent.viteConfig, config)
553552
sibling.runner = parent.runner
554553
sibling._resolver = parent._resolver
555-
sibling._fetcher = parent._fetcher
554+
sibling._transformService = parent._transformService
556555
sibling._parent = parent
557556
sibling._serializedDefines = parent._serializedDefines
558557
return sibling

‎packages/vitest/src/node/specifications.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ export class VitestSpecifications {
219219
const environment = project.vite.environments.ssr
220220
const mod = environment.moduleGraph.getModuleById(filepath)
221221
const transformed =
222-
mod?.transformResult || (await withLimit(() => environment.transformRequest(filepath)))
222+
mod?.transformResult ||
223+
(await withLimit(() => project._transformService.transform(filepath, environment)))
223224
if (!transformed) {
224225
return
225226
}

‎test/e2e/test/caching.test.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,79 @@ test.each([
194194
}
195195
})
196196

197+
test('filtering by related sources reuses the cache', async () => {
198+
const transformed: string[] = []
199+
;(globalThis as any).__vitest_transformed__ = transformed
200+
201+
const cold = await runInlineTests(
202+
{
203+
'vitest.config.js': `
204+
import { defineConfig } from 'vitest/config'
205+
export default defineConfig({
206+
plugins: [{
207+
name: 'track-transforms',
208+
transform(_code, id) {
209+
if (id.startsWith(this.environment.config.root)) {
210+
globalThis.__vitest_transformed__.push(id.slice(this.environment.config.root.length + 1))
211+
}
212+
},
213+
}],
214+
test: {
215+
fsModuleCache: true,
216+
fsModuleCachePath: './node_modules/.vitest-fs-cache',
217+
},
218+
})
219+
`,
220+
'a.js': `export const a = 'a'`,
221+
'b.js': `export const b = 'b'`,
222+
'a.test.js': `
223+
import { expect, test } from 'vitest'
224+
import { a } from './a.js'
225+
test('a', () => expect(a).toBe('a'))
226+
`,
227+
'b.test.js': `
228+
import { expect, test } from 'vitest'
229+
import { b } from './b.js'
230+
test('b', () => expect(b).toBe('b'))
231+
`,
232+
},
233+
{ related: ['a.js'] },
234+
)
235+
236+
expect(cold.stderr).toBe('')
237+
expect(cold.testTree()).toMatchInlineSnapshot(`
238+
{
239+
"a.test.js": {
240+
"a": "passed",
241+
},
242+
}
243+
`)
244+
expect(transformed.sort()).toMatchInlineSnapshot(`
245+
[
246+
"a.js",
247+
"a.test.js",
248+
"b.js",
249+
"b.test.js",
250+
]
251+
`)
252+
253+
transformed.length = 0
254+
255+
const warm = await runVitest({ root: cold.root, related: ['a.js'] })
256+
257+
expect(warm.stderr).toBe('')
258+
expect(warm.testTree()).toMatchInlineSnapshot(`
259+
{
260+
"a.test.js": {
261+
"a": "passed",
262+
},
263+
}
264+
`)
265+
expect(transformed).toEqual([])
266+
267+
delete (globalThis as any).__vitest_transformed__
268+
})
269+
197270
test('if cache key generator bails out, the file is not cached', async () => {
198271
process.env.REPLACED = 'value1'
199272

0 commit comments

Comments
 (0)