Skip to content

Commit 931c0ee

Browse files
authored
feat: add a way to dump transformed content (#8711)
1 parent f6690ed commit 931c0ee

File tree

7 files changed

+156
-37
lines changed

7 files changed

+156
-37
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ test/cli/fixtures/browser-multiple/basic-*
3434
test/browser/html/
3535
test/core/html/
3636
.vitest-attachments
37-
explainFiles.txt
37+
explainFiles.txt
38+
.vitest-dump

packages/vitest/src/node/config/resolveConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,17 @@ export function resolveConfig(
831831
resolved.server ??= {}
832832
resolved.server.deps ??= {}
833833

834+
if (resolved.server.debug?.dump || process.env.VITEST_DEBUG_DUMP) {
835+
const userFolder = resolved.server.debug?.dump || process.env.VITEST_DEBUG_DUMP
836+
resolved.dumpDir = resolve(
837+
resolved.root,
838+
typeof userFolder === 'string' && userFolder !== 'true'
839+
? userFolder
840+
: '.vitest-dump',
841+
resolved.name || 'root',
842+
)
843+
}
844+
834845
resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000
835846
resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000
836847

packages/vitest/src/node/environments/fetchModule.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ import type { DevEnvironment, FetchResult, Rollup, TransformResult } from 'vite'
22
import type { FetchFunctionOptions } from 'vite/module-runner'
33
import type { FetchCachedFileSystemResult } from '../../types/general'
44
import type { VitestResolver } from '../resolver'
5-
import { mkdirSync } from 'node:fs'
6-
import { rename, stat, unlink, writeFile } from 'node:fs/promises'
5+
import { existsSync, mkdirSync } from 'node:fs'
6+
import { readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
77
import { tmpdir } from 'node:os'
88
import { isExternalUrl, nanoid, unwrapId } from '@vitest/utils/helpers'
9-
import { dirname, join } from 'pathe'
9+
import { dirname, join, resolve } from 'pathe'
1010
import { fetchModule } from 'vite'
1111
import { hash } from '../hash'
1212

1313
const created = new Set()
1414
const promises = new Map<string, Promise<void>>()
1515

16+
interface DumpOptions {
17+
dumpFolder?: string
18+
readFromDump?: boolean
19+
}
20+
1621
export function createFetchModuleFunction(
1722
resolver: VitestResolver,
1823
cacheFs: boolean = false,
1924
tmpDir: string = join(tmpdir(), nanoid()),
25+
dump?: DumpOptions,
2026
): (
2127
url: string,
2228
importer: string | undefined,
@@ -67,18 +73,45 @@ export function createFetchModuleFunction(
6773
}
6874
}
6975

70-
const moduleRunnerModule = await fetchModule(
71-
environment,
72-
url,
73-
importer,
74-
{
75-
...options,
76-
inlineSourceMap: false,
77-
},
78-
).catch(handleRollupError)
76+
let moduleRunnerModule: FetchResult | undefined
77+
78+
if (dump?.dumpFolder && dump.readFromDump) {
79+
const path = resolve(dump?.dumpFolder, url.replace(/[^\w+]/g, '-'))
80+
if (existsSync(path)) {
81+
const code = await readFile(path, 'utf-8')
82+
const matchIndex = code.lastIndexOf('\n//')
83+
if (matchIndex !== -1) {
84+
const { id, file } = JSON.parse(code.slice(matchIndex + 4))
85+
moduleRunnerModule = {
86+
code,
87+
id,
88+
url,
89+
file,
90+
invalidate: false,
91+
}
92+
}
93+
}
94+
}
95+
96+
if (!moduleRunnerModule) {
97+
moduleRunnerModule = await fetchModule(
98+
environment,
99+
url,
100+
importer,
101+
{
102+
...options,
103+
inlineSourceMap: false,
104+
},
105+
).catch(handleRollupError)
106+
}
79107

80108
const result = processResultSource(environment, moduleRunnerModule)
81109

110+
if (dump?.dumpFolder && 'code' in result) {
111+
const path = resolve(dump?.dumpFolder, result.url.replace(/[^\w+]/g, '-'))
112+
await writeFile(path, `${result.code}\n// ${JSON.stringify({ id: result.id, file: result.file })}`, 'utf-8')
113+
}
114+
82115
if (!cacheFs || !('code' in result)) {
83116
return result
84117
}

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

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RuntimeRPC } from '../../types/rpc'
22
import type { TestProject } from '../project'
33
import type { ResolveSnapshotPathHandlerContext } from '../types/config'
4+
import { existsSync, mkdirSync } from 'node:fs'
45
import { fileURLToPath } from 'node:url'
56
import { cleanUrl } from '@vitest/utils/helpers'
67
import { createFetchModuleFunction, handleRollupError } from '../environments/fetchModule'
@@ -13,9 +14,26 @@ interface MethodsOptions {
1314
}
1415

1516
export function createMethodsRPC(project: TestProject, options: MethodsOptions = {}): RuntimeRPC {
16-
const ctx = project.vitest
17+
const vitest = project.vitest
1718
const cacheFs = options.cacheFs ?? false
18-
const fetch = createFetchModuleFunction(project._resolver, cacheFs, project.tmpDir)
19+
project.vitest.state.metadata[project.name] ??= {
20+
externalized: {},
21+
duration: {},
22+
tmps: {},
23+
}
24+
if (project.config.dumpDir && !existsSync(project.config.dumpDir)) {
25+
mkdirSync(project.config.dumpDir, { recursive: true })
26+
}
27+
project.vitest.state.metadata[project.name].dumpDir = project.config.dumpDir
28+
const fetch = createFetchModuleFunction(
29+
project._resolver,
30+
cacheFs,
31+
project.tmpDir,
32+
{
33+
dumpFolder: project.config.dumpDir,
34+
readFromDump: project.config.server.debug?.load ?? process.env.VITEST_DEBUG_LOAD_DUMP != null,
35+
},
36+
)
1937
return {
2038
async fetch(
2139
url,
@@ -30,12 +48,20 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
3048

3149
const start = performance.now()
3250

33-
try {
34-
return await fetch(url, importer, environment, options)
35-
}
36-
finally {
37-
project.vitest.state.transformTime += (performance.now() - start)
38-
}
51+
return await fetch(url, importer, environment, options).then((result) => {
52+
const duration = performance.now() - start
53+
project.vitest.state.transformTime += duration
54+
const metadata = project.vitest.state.metadata[project.name]
55+
if ('externalize' in result) {
56+
metadata.externalized[url] = result.externalize
57+
}
58+
if ('tmp' in result) {
59+
metadata.tmps[url] = result.tmp
60+
}
61+
metadata.duration[url] ??= []
62+
metadata.duration[url].push(duration)
63+
return result
64+
})
3965
},
4066
async resolve(id, importer, environmentName) {
4167
const environment = project.vite.environments[environmentName]
@@ -54,10 +80,10 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
5480
},
5581

5682
snapshotSaved(snapshot) {
57-
ctx.snapshot.add(snapshot)
83+
vitest.snapshot.add(snapshot)
5884
},
5985
resolveSnapshotPath(testPath: string) {
60-
return ctx.snapshot.resolvePath<ResolveSnapshotPathHandlerContext>(testPath, {
86+
return vitest.snapshot.resolvePath<ResolveSnapshotPathHandlerContext>(testPath, {
6187
config: project.serializedConfig,
6288
})
6389
},
@@ -73,50 +99,50 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
7399
},
74100
async onQueued(file) {
75101
if (options.collect) {
76-
ctx.state.collectFiles(project, [file])
102+
vitest.state.collectFiles(project, [file])
77103
}
78104
else {
79-
await ctx._testRun.enqueued(project, file)
105+
await vitest._testRun.enqueued(project, file)
80106
}
81107
},
82108
async onCollected(files) {
83109
if (options.collect) {
84-
ctx.state.collectFiles(project, files)
110+
vitest.state.collectFiles(project, files)
85111
}
86112
else {
87-
await ctx._testRun.collected(project, files)
113+
await vitest._testRun.collected(project, files)
88114
}
89115
},
90116
onAfterSuiteRun(meta) {
91-
ctx.coverageProvider?.onAfterSuiteRun(meta)
117+
vitest.coverageProvider?.onAfterSuiteRun(meta)
92118
},
93119
async onTaskAnnotate(testId, annotation) {
94-
return ctx._testRun.annotate(testId, annotation)
120+
return vitest._testRun.annotate(testId, annotation)
95121
},
96122
async onTaskUpdate(packs, events) {
97123
if (options.collect) {
98-
ctx.state.updateTasks(packs)
124+
vitest.state.updateTasks(packs)
99125
}
100126
else {
101-
await ctx._testRun.updated(packs, events)
127+
await vitest._testRun.updated(packs, events)
102128
}
103129
},
104130
async onUserConsoleLog(log) {
105131
if (options.collect) {
106-
ctx.state.updateUserLog(log)
132+
vitest.state.updateUserLog(log)
107133
}
108134
else {
109-
await ctx._testRun.log(log)
135+
await vitest._testRun.log(log)
110136
}
111137
},
112138
onUnhandledError(err, type) {
113-
ctx.state.catchError(err, type)
139+
vitest.state.catchError(err, type)
114140
},
115141
onCancel(reason) {
116-
ctx.cancelCurrentRun(reason)
142+
vitest.cancelCurrentRun(reason)
117143
},
118144
getCountOfFailedTests() {
119-
return ctx.state.getCountOfFailedTests()
145+
return vitest.state.getCountOfFailedTests()
120146
},
121147
}
122148
}

packages/vitest/src/node/state.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ export class StateManager {
2626
blobs?: MergedBlobs
2727
transformTime = 0
2828

29+
metadata: Record<string, {
30+
externalized: Record<string, string>
31+
duration: Record<string, number[]>
32+
tmps: Record<string, string>
33+
dumpDir?: string
34+
outline?: {
35+
externalized: number
36+
inlined: number
37+
}
38+
}> = {}
39+
2940
onUnhandledError?: OnUnhandledErrorCallback
3041

3142
/** @internal */

packages/vitest/src/node/test-run.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { TestRunEndReason } from './types/reporter'
1717
import assert from 'node:assert'
1818
import { createHash } from 'node:crypto'
1919
import { existsSync } from 'node:fs'
20-
import { copyFile, mkdir } from 'node:fs/promises'
20+
import { copyFile, mkdir, writeFile } from 'node:fs/promises'
2121
import { isPrimitive } from '@vitest/utils/helpers'
2222
import { serializeValue } from '@vitest/utils/serialize'
2323
import { parseErrorStacktrace } from '@vitest/utils/source-map'
@@ -107,6 +107,24 @@ export class TestRun {
107107
}
108108

109109
await this.vitest.report('onTestRunEnd', modules, [...errors] as SerializedError[], state)
110+
111+
for (const project in this.vitest.state.metadata) {
112+
const meta = this.vitest.state.metadata[project]
113+
if (!meta?.dumpDir) {
114+
continue
115+
}
116+
const path = resolve(meta.dumpDir, 'vitest-metadata.json')
117+
meta.outline = {
118+
externalized: Object.keys(meta.externalized).length,
119+
inlined: Object.keys(meta.tmps).length,
120+
}
121+
await writeFile(
122+
path,
123+
JSON.stringify(meta, null, 2),
124+
'utf-8',
125+
)
126+
this.vitest.logger.log(`Metadata written to ${path}`)
127+
}
110128
}
111129

112130
private hasFailed(modules: TestModule[]) {

packages/vitest/src/node/types/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,23 @@ export interface InlineConfig {
246246

247247
server?: {
248248
deps?: ServerDepsOptions
249+
debug?: {
250+
/**
251+
* The folder where Vitest stores the contents of transformed
252+
* test files that can be inspected manually.
253+
*
254+
* If `true`, Vitest dumps the files in `.vitest-dump` folder relative to the root of the project.
255+
*
256+
* You can also use `VITEST_DEBUG_DUMP` env variable to enable this.
257+
*/
258+
dump?: string | true
259+
/**
260+
* If dump is enabled, should Vitest load the files from there instead of transforming them.
261+
*
262+
* You can also use `VITEST_DEBUG_LOAD_DUMP` env variable to enable this.
263+
*/
264+
load?: boolean
265+
}
249266
}
250267

251268
/**
@@ -1010,6 +1027,8 @@ export interface ResolvedConfig
10101027
runner?: string
10111028

10121029
maxWorkers: number
1030+
1031+
dumpDir?: string
10131032
}
10141033

10151034
type NonProjectOptions

0 commit comments

Comments
 (0)