diff --git a/packages/vitest/src/node/ast-collect.ts b/packages/vitest/src/node/ast-collect.ts index 28ad22a0f..7dab9bc4f 100644 --- a/packages/vitest/src/node/ast-collect.ts +++ b/packages/vitest/src/node/ast-collect.ts @@ -16,7 +16,7 @@ import { createTaskName, } from '../utils/tasks' import { detectCodeBlock } from '../utils/test-helpers' -import { toRollupError } from './environments/fetchModule' +import { toRollupError } from './environments/transformService' interface ParsedFile extends File { start: number diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 9f005d4ac..4088a8934 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -7,7 +7,6 @@ import type { CancelReason, File } from '../runtime/runner/types' import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general' import type { SourceModuleDiagnostic, SourceModuleLocations } from '../types/module-locations' import type { PluginHarness } from './config/pluginHarness' -import type { VitestFetchFunction } from './environments/fetchModule' import type { Logger } from './logger' import type { VitestPackageInstaller } from './packageInstaller' import type { ProcessPool } from './pool' @@ -41,8 +40,8 @@ import { VitestCache } from './cache' import { FileSystemModuleCache } from './cache/fsModuleCache' import { matchesProjectFilter, resolveConfig } from './config/resolveConfig' import { getCoverageProvider } from './coverage' -import { createFetchModuleFunction } from './environments/fetchModule' import { ServerModuleRunner } from './environments/serverRunner' +import { ModuleTransformService } from './environments/transformService' import { FilesNotFoundError } from './errors' import { collectModuleDurationsDiagnostic, @@ -169,7 +168,7 @@ export class Vitest { /** @internal */ runner!: ModuleRunner /** @internal */ _testRun: TestRun /** @internal */ _resolver!: VitestResolver - /** @internal */ _fetcher!: VitestFetchFunction + /** @internal */ _transformService!: ModuleTransformService /** @internal */ _fsCache!: FileSystemModuleCache /** @internal */ _tmpDir: string | undefined = join(tmpdir(), nanoid()) /** @internal */ _traces!: Traces @@ -256,7 +255,7 @@ export class Vitest { this._resolver = new VitestResolver(this.viteConfig.cacheDir, resolved) // a closed run removes the temp dir, so a restart must allocate a new one const tmpDir = (this._tmpDir ??= join(tmpdir(), nanoid())) - this._fetcher = createFetchModuleFunction( + this._transformService = new ModuleTransformService( this._resolver, resolved, this._fsCache, @@ -324,7 +323,7 @@ export class Vitest { this.runner = resolved.experimental.viteModuleRunner === false ? new NativeModuleRunner(resolved.root) - : new ServerModuleRunner(environment, this._fetcher, resolved) + : new ServerModuleRunner(environment, this._transformService, resolved) this.vcs = await loadVCSProvider(this.runner, resolved.experimental.vcsProvider) if (resolved.watch) { diff --git a/packages/vitest/src/node/environments/serverRunner.ts b/packages/vitest/src/node/environments/serverRunner.ts index 091c16b25..34a2c5e5a 100644 --- a/packages/vitest/src/node/environments/serverRunner.ts +++ b/packages/vitest/src/node/environments/serverRunner.ts @@ -1,6 +1,6 @@ import type { DevEnvironment, ViteDevServer } from 'vite' import type { ResolvedConfig } from '../types/config' -import type { VitestFetchFunction } from './fetchModule' +import type { ModuleTransformService } from './transformService' import { readFile } from 'node:fs/promises' import { isRunnableDevEnvironment } from 'vite' import { ModuleRunner } from 'vite/module-runner' @@ -10,7 +10,7 @@ import { normalizeResolvedIdToUrl } from './normalizeUrl' export class ServerModuleRunner extends ModuleRunner { constructor( private environment: DevEnvironment, - fetcher: VitestFetchFunction, + transformService: ModuleTransformService, private config: ResolvedConfig, ) { super( @@ -29,7 +29,13 @@ export class ServerModuleRunner extends ModuleRunner { return { error: new Error(`Unknown method: ${name}. Expected "fetchModule".`) } } try { - const result = await fetcher(data[0], data[1], environment, false, data[2]) + const result = await transformService.fetch( + data[0], + data[1], + environment, + false, + data[2], + ) if ('tmp' in result) { const code = await readFile(result.tmp) return { result: { ...result, code } } @@ -61,14 +67,14 @@ export class ServerModuleRunner extends ModuleRunner { // call `server.environments.ssr.runner.import(...)` get Vitest's module runner. export function installSsrModuleRunner( server: ViteDevServer, - fetcher: VitestFetchFunction, + transformService: ModuleTransformService, config: ResolvedConfig, ): void { const ssrEnvironment = server.environments.ssr if (!isRunnableDevEnvironment(ssrEnvironment)) { return } - const ssrRunner = new ServerModuleRunner(ssrEnvironment, fetcher, config) + const ssrRunner = new ServerModuleRunner(ssrEnvironment, transformService, config) Object.defineProperty(ssrEnvironment, 'runner', { value: ssrRunner, writable: true, diff --git a/packages/vitest/src/node/environments/fetchModule.ts b/packages/vitest/src/node/environments/transformService.ts similarity index 94% rename from packages/vitest/src/node/environments/fetchModule.ts rename to packages/vitest/src/node/environments/transformService.ts index f58fe5f82..40c5096d4 100644 --- a/packages/vitest/src/node/environments/fetchModule.ts +++ b/packages/vitest/src/node/environments/transformService.ts @@ -42,7 +42,7 @@ const saveCachePromises = new Map< >() const readFilePromises = new Map>() -class ModuleFetcher { +export class ModuleTransformService { private tmpDirectories = new Set() private fsCacheEnabled: boolean // the module type is only needed by the evaluator to decide if CJS @@ -54,6 +54,7 @@ class ModuleFetcher { private resolver: VitestResolver, private config: ResolvedConfig, private fsCache: FileSystemModuleCache, + private traces: Traces, private tmpProjectDir: string, ) { this.fsCacheEnabled = config.fsModuleCache === true @@ -61,6 +62,40 @@ class ModuleFetcher { } async fetch( + url: string, + importer: string | undefined, + environment: DevEnvironment, + makeTmpCopies?: boolean, + options?: FetchFunctionOptions, + otelCarrier?: OTELCarrier, + ): Promise { + await this.traces.waitInit() + const context = otelCarrier ? this.traces.getContextFromCarrier(otelCarrier) : undefined + return this.traces.$('vitest.module.transform', context ? { context } : {}, (span) => + this.fetchInSpan(span, url, importer, environment, makeTmpCopies, options), + ) + } + + /** + * Transforms the module like `environment.transformRequest`, but reads and + * populates the `fsModuleCache` when it is enabled. + */ + async transform(id: string, environment: DevEnvironment): Promise { + await this.traces.waitInit() + const result = await this.traces.$('vitest.module.transform', (span) => + this.fetchInSpan(span, id, undefined, environment), + ) + if ('id' in result) { + const transformResult = environment.moduleGraph.getModuleById(result.id)?.transformResult + if (transformResult) { + return transformResult + } + } + // externalized modules are never transformed by `fetch` + return environment.transformRequest(id) + } + + private async fetchInSpan( trace: Span, url: string, importer: string | undefined, @@ -531,34 +566,6 @@ function getSafeModulePaths(environment: DevEnvironment): Set | undefine return config.safeModulePaths } -export interface VitestFetchFunction { - ( - url: string, - importer: string | undefined, - environment: DevEnvironment, - cacheFs?: boolean, - options?: FetchFunctionOptions, - otelCarrier?: OTELCarrier, - ): Promise -} - -export function createFetchModuleFunction( - resolver: VitestResolver, - config: ResolvedConfig, - fsCache: FileSystemModuleCache, - traces: Traces, - tmpProjectDir: string, -): VitestFetchFunction { - const fetcher = new ModuleFetcher(resolver, config, fsCache, tmpProjectDir) - return async (url, importer, environment, cacheFs, options, otelCarrier) => { - await traces.waitInit() - const context = otelCarrier ? traces.getContextFromCarrier(otelCarrier) : undefined - return traces.$('vitest.module.transform', context ? { context } : {}, (span) => - fetcher.fetch(span, url, importer, environment, cacheFs, options), - ) - } -} - let SOURCEMAPPING_URL = 'sourceMa' SOURCEMAPPING_URL += 'ppingURL' diff --git a/packages/vitest/src/node/plugins/ssrRunnerFixer.ts b/packages/vitest/src/node/plugins/ssrRunnerFixer.ts index c55c3d81d..31f5c271c 100644 --- a/packages/vitest/src/node/plugins/ssrRunnerFixer.ts +++ b/packages/vitest/src/node/plugins/ssrRunnerFixer.ts @@ -12,7 +12,7 @@ export function SsrRunnerFixerPlugin(harness: PluginHarness): Plugin { order: 'pre', handler(server) { const vitest = harness.getVitest() - installSsrModuleRunner(server, vitest._fetcher, vitest.config) + installSsrModuleRunner(server, vitest._transformService, vitest.config) }, }, } diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 03c326560..febcf521a 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -10,8 +10,8 @@ import { existsSync, mkdirSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { cleanUrl } from '@vitest/utils/helpers' import { isBuiltin, toBuiltin } from '../../utils/modules' -import { handleRollupError } from '../environments/fetchModule' import { normalizeResolvedIdToUrl } from '../environments/normalizeUrl' +import { handleRollupError } from '../environments/transformService' interface MethodsOptions { cacheFs?: boolean @@ -70,8 +70,8 @@ export function createMethodsRPC( const state = project.vitest.state const start = performance.now() - return await project - ._fetcher(url, importer, environment, cacheFs, options, otelCarrier) + return await project._transformService + .fetch(url, importer, environment, cacheFs, options, otelCarrier) .then((result) => { const metadata = state.metadata[project.name] if ('externalize' in result) { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index e43e36f89..e6a9bdaaf 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -3,7 +3,6 @@ import type { ModuleRunner } from 'vite/module-runner' import type { Typechecker } from '../typecheck/typechecker' import type { ProvidedContext } from '../types/general' import type { OnTestsRerunHandler, Vitest } from './core' -import type { VitestFetchFunction } from './environments/fetchModule' import type { GlobalSetupFile } from './globalSetup' import type { TestSpecificationOptions } from './test-specification' import type { ParentProjectBrowser, ProjectBrowser } from './types/browser' @@ -19,8 +18,8 @@ import { createDefinesScript } from '../utils/config-helpers' import { NativeModuleRunner } from '../utils/nativeModuleRunner' import { BenchmarkManager } from './benchmark' import { serializeConfig } from './config/serializeConfig' -import { createFetchModuleFunction } from './environments/fetchModule' import { ServerModuleRunner } from './environments/serverRunner' +import { ModuleTransformService } from './environments/transformService' import { loadGlobalSetupFiles } from './globalSetup' import { listenClusterServer } from './plugins/browserLoader' import { getFilePoolName } from './pool' @@ -62,7 +61,7 @@ export class TestProject { /** @internal */ typechecker?: Typechecker /** @internal */ _resolver!: VitestResolver - /** @internal */ _fetcher!: VitestFetchFunction + /** @internal */ _transformService!: ModuleTransformService /** @internal */ _serializedDefines?: string /** @internal */ _sharedViteServer = false /** @internal */ testFilesList: string[] | null = null @@ -97,7 +96,7 @@ export class TestProject { _initializeRunners(server: ViteDevServer) { this._serializedDefines = createDefinesScript(this.config._scriptDefines) this._resolver = new VitestResolver(server.config.cacheDir, this.config) - this._fetcher = createFetchModuleFunction( + this._transformService = new ModuleTransformService( this._resolver, this.config, this.vitest._fsCache, @@ -109,7 +108,7 @@ export class TestProject { this.runner = this.config.experimental.viteModuleRunner === false ? new NativeModuleRunner(this.config.root) - : new ServerModuleRunner(environment, this._fetcher, this.config) + : new ServerModuleRunner(environment, this._transformService, this.config) } // "provide" is a property, not a method to keep the context when destructed in the global setup, @@ -533,14 +532,14 @@ export class TestProject { const project = new TestProject(vitest, vitest.vite, vitest.viteConfig, vitest.config) project.runner = vitest.runner project._resolver = vitest._resolver - project._fetcher = vitest._fetcher + project._transformService = vitest._transformService project._serializedDefines = createDefinesScript(vitest.config._scriptDefines) return project } /** * Create a sibling project that shares server-derived resources (Vite server, - * runner, resolver, fetcher) with a primary project. The sibling has its own + * runner, resolver, transform service) with a primary project. The sibling has its own * distinct `projectConfig`, but the same `viteConfig` reference as the primary. * * Used for browser-instance and benchmark variants whose entries share a @@ -552,7 +551,7 @@ export class TestProject { const sibling = new TestProject(parent.vitest, parent.vite, parent.viteConfig, config) sibling.runner = parent.runner sibling._resolver = parent._resolver - sibling._fetcher = parent._fetcher + sibling._transformService = parent._transformService sibling._parent = parent sibling._serializedDefines = parent._serializedDefines return sibling diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index e2af1b444..16d25e7cc 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -219,7 +219,8 @@ export class VitestSpecifications { const environment = project.vite.environments.ssr const mod = environment.moduleGraph.getModuleById(filepath) const transformed = - mod?.transformResult || (await withLimit(() => environment.transformRequest(filepath))) + mod?.transformResult || + (await withLimit(() => project._transformService.transform(filepath, environment))) if (!transformed) { return } diff --git a/test/e2e/test/caching.test.ts b/test/e2e/test/caching.test.ts index fd65ef882..a59b57633 100644 --- a/test/e2e/test/caching.test.ts +++ b/test/e2e/test/caching.test.ts @@ -194,6 +194,79 @@ test.each([ } }) +test('filtering by related sources reuses the cache', async () => { + const transformed: string[] = [] + ;(globalThis as any).__vitest_transformed__ = transformed + + const cold = await runInlineTests( + { + 'vitest.config.js': ` + import { defineConfig } from 'vitest/config' + export default defineConfig({ + plugins: [{ + name: 'track-transforms', + transform(_code, id) { + if (id.startsWith(this.environment.config.root)) { + globalThis.__vitest_transformed__.push(id.slice(this.environment.config.root.length + 1)) + } + }, + }], + test: { + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + }, + }) + `, + 'a.js': `export const a = 'a'`, + 'b.js': `export const b = 'b'`, + 'a.test.js': ` + import { expect, test } from 'vitest' + import { a } from './a.js' + test('a', () => expect(a).toBe('a')) + `, + 'b.test.js': ` + import { expect, test } from 'vitest' + import { b } from './b.js' + test('b', () => expect(b).toBe('b')) + `, + }, + { related: ['a.js'] }, + ) + + expect(cold.stderr).toBe('') + expect(cold.testTree()).toMatchInlineSnapshot(` + { + "a.test.js": { + "a": "passed", + }, + } + `) + expect(transformed.sort()).toMatchInlineSnapshot(` + [ + "a.js", + "a.test.js", + "b.js", + "b.test.js", + ] + `) + + transformed.length = 0 + + const warm = await runVitest({ root: cold.root, related: ['a.js'] }) + + expect(warm.stderr).toBe('') + expect(warm.testTree()).toMatchInlineSnapshot(` + { + "a.test.js": { + "a": "passed", + }, + } + `) + expect(transformed).toEqual([]) + + delete (globalThis as any).__vitest_transformed__ +}) + test('if cache key generator bails out, the file is not cached', async () => { process.env.REPLACED = 'value1'