Skip to content

Commit c663f39

Browse files
committedAug 16, 2022
feat: restart vitest on config change
1 parent 14279d5 commit c663f39

File tree

9 files changed

+103
-55
lines changed

9 files changed

+103
-55
lines changed
 

‎packages/vitest/src/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const distDir = resolve(url.fileURLToPath(import.meta.url), '../../dist')
77
// if changed, update also jsdocs and docs
88
export const defaultPort = 51204
99

10+
export const EXIT_CODE_RESTART = 43
11+
1012
export const API_PATH = '/__vitest_api__'
1113

1214
export const configFiles = [

‎packages/vitest/src/node/cli-api.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolve } from 'pathe'
22
import type { UserConfig as ViteUserConfig } from 'vite'
3+
import { EXIT_CODE_RESTART } from '../constants'
34
import { CoverageProviderMap } from '../integrations/coverage'
45
import { envPackageNames } from '../integrations/env'
56
import type { UserConfig } from '../types'
@@ -60,9 +61,13 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit
6061
if (process.stdin.isTTY && ctx.config.watch)
6162
registerConsoleShortcuts(ctx)
6263

63-
ctx.onServerRestarted(() => {
64-
// TODO: re-consider how to re-run the tests the server smartly
65-
ctx.start(cliFilters)
64+
ctx.onServerRestart((reason) => {
65+
ctx.report('onServerRestart', reason)
66+
67+
if (process.env.VITEST_CLI_WRAPPER)
68+
process.exit(EXIT_CODE_RESTART)
69+
else
70+
ctx.start(cliFilters)
6671
})
6772

6873
try {

‎packages/vitest/src/node/cli-wrapper.ts

+59-31
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
import { fileURLToPath } from 'url'
66
import c from 'picocolors'
77
import { execa } from 'execa'
8+
import { EXIT_CODE_RESTART } from '../constants'
89

910
const ENTRY = new URL('./cli.mjs', import.meta.url)
10-
const NODE_ARGS = ['--inspect', '--inspect-brk', '--trace-deprecation']
11+
12+
/** Arguments passed to Node before the script */
13+
const NODE_ARGS = [
14+
'--inspect',
15+
'--inspect-brk',
16+
'--trace-deprecation',
17+
]
1118

1219
interface ErrorDef {
1320
trigger: string
1421
url: string
1522
}
1623

17-
// Node errors seen in Vitest (vitejs/vite#9492)
18-
const ERRORS: ErrorDef[] = [
24+
const SegfaultErrors: ErrorDef[] = [
1925
{
2026
trigger: 'Check failed: result.second.',
2127
url: 'https://github.com/nodejs/node/issues/43617',
@@ -30,9 +36,10 @@ const ERRORS: ErrorDef[] = [
3036
},
3137
]
3238

39+
main()
40+
3341
async function main() {
3442
// default exit code = 100, as in retries were exhausted
35-
const exitCode = 100
3643
let retries = 0
3744
const args = process.argv.slice(2)
3845

@@ -52,24 +59,6 @@ async function main() {
5259
}
5360
}
5461

55-
retries = Math.max(1, retries || 1)
56-
57-
for (let i = 1; i <= retries; i++) {
58-
if (i !== 1)
59-
console.log(`${c.inverse(c.bold(c.magenta(' Retrying ')))} vitest ${args.join(' ')} ${c.gray(`(${i} of ${retries})`)}`)
60-
await start(args)
61-
if (i === 1 && retries === 1) {
62-
console.log(c.yellow(`It seems to be an upstream bug of Node.js. To improve the test stability,
63-
you could pass ${c.bold(c.green('--segfault-retry=3'))} or set env ${c.bold(c.green('VITEST_SEGFAULT_RETRY=3'))} to
64-
have Vitest auto retries on flaky segfaults.\n`))
65-
}
66-
}
67-
process.exit(exitCode)
68-
}
69-
70-
main()
71-
72-
async function start(args: string[]) {
7362
const nodeArgs: string[] = []
7463
const vitestArgs: string[] = []
7564

@@ -87,23 +76,62 @@ async function start(args: string[]) {
8776
vitestArgs.push(args[i])
8877
}
8978

90-
const child = execa('node', [...nodeArgs, fileURLToPath(ENTRY), ...vitestArgs], {
91-
reject: false,
92-
stderr: 'pipe',
93-
stdout: 'inherit',
94-
stdin: 'inherit',
95-
})
79+
retries = Math.max(1, retries || 1)
80+
81+
for (let i = 1; i <= retries; i++) {
82+
const result = await start(nodeArgs, vitestArgs)
83+
84+
if (result === 'restart') {
85+
i -= 1
86+
continue
87+
}
88+
89+
if (i === 1 && retries === 1) {
90+
console.log(c.yellow(`It seems to be an upstream bug of Node.js. To improve the test stability,
91+
you could pass ${c.bold(c.green('--segfault-retry=3'))} or set env ${c.bold(c.green('VITEST_SEGFAULT_RETRY=3'))} to
92+
have Vitest auto retries on flaky segfaults.\n`))
93+
}
94+
95+
if (i !== retries)
96+
console.log(`${c.inverse(c.bold(c.magenta(' Retrying ')))} vitest ${args.join(' ')} ${c.gray(`(${i + 1} of ${retries})`)}`)
97+
}
98+
99+
// retry out
100+
process.exit(1)
101+
}
102+
103+
async function start(preArgs: string[], postArgs: string[]) {
104+
const child = execa(
105+
'node',
106+
[
107+
...preArgs,
108+
fileURLToPath(ENTRY),
109+
...postArgs,
110+
],
111+
{
112+
reject: false,
113+
stderr: 'pipe',
114+
stdout: 'inherit',
115+
stdin: 'inherit',
116+
env: {
117+
...process.env,
118+
VITEST_CLI_WRAPPER: 'true',
119+
},
120+
},
121+
)
96122
child.stderr?.pipe(process.stderr)
97123
const { stderr = '' } = await child
98124

99-
for (const error of ERRORS) {
125+
if (child.exitCode === EXIT_CODE_RESTART)
126+
return 'restart'
127+
128+
for (const error of SegfaultErrors) {
100129
if (stderr.includes(error.trigger)) {
101130
if (process.env.GITHUB_ACTIONS)
102131
console.log(`::warning:: Segmentfault Error Detected: ${error.trigger}\nRefer to ${error.url}`)
103-
104132
const RED_BLOCK = c.inverse(c.red(' '))
105133
console.log(`\n${c.inverse(c.bold(c.red(' Segmentfault Error Detected ')))}\n${RED_BLOCK} ${c.red(error.trigger)}\n${RED_BLOCK} ${c.red(`Refer to ${error.url}`)}\n`)
106-
return
134+
return 'error'
107135
}
108136
}
109137

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

+23-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { existsSync, promises as fs } from 'fs'
22
import type { ViteDevServer } from 'vite'
3+
import { normalizePath } from 'vite'
34
import { relative, toNamespacedPath } from 'pathe'
45
import fg from 'fast-glob'
56
import mm from 'micromatch'
67
import c from 'picocolors'
78
import { ViteNodeRunner } from 'vite-node/client'
89
import { ViteNodeServer } from 'vite-node/server'
9-
import type { ArgumentsType, CoverageProvider, Reporter, ResolvedConfig, UserConfig } from '../types'
10+
import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig } from '../types'
1011
import { SnapshotManager } from '../integrations/snapshot/manager'
1112
import { clearTimeout, deepMerge, hasFailed, noop, setTimeout, slash } from '../utils'
1213
import { getCoverageProvider } from '../integrations/coverage'
@@ -48,7 +49,7 @@ export class Vitest {
4849
this.logger = new Logger(this)
4950
}
5051

51-
private _onRestartListeners: Array<() => void> = []
52+
private _onRestartListeners: OnServerRestartHandler[] = []
5253

5354
async setServer(options: UserConfig, server: ViteDevServer) {
5455
this.unregisterWatcher?.()
@@ -81,12 +82,29 @@ export class Vitest {
8182
},
8283
})
8384

85+
if (this.config.watch) {
86+
// hijack server restart
87+
const serverRestart = server.restart
88+
server.restart = async (...args) => {
89+
await Promise.all(this._onRestartListeners.map(fn => fn()))
90+
return await serverRestart(...args)
91+
}
92+
93+
// since we set `server.hmr: false`, Vite does not auto restart itself
94+
server.watcher.on('change', async (file) => {
95+
file = normalizePath(file)
96+
const isConfig = file === server.config.configFile
97+
if (isConfig) {
98+
await Promise.all(this._onRestartListeners.map(fn => fn('config')))
99+
await serverRestart()
100+
}
101+
})
102+
}
103+
84104
this.reporters = await createReporters(resolved.reporters, this.runner)
85105

86106
this.runningPromise = undefined
87107

88-
this._onRestartListeners.forEach(fn => fn())
89-
90108
await this.coverageProvider?.clean(this.config.coverage.clean)
91109

92110
this.cache.results.setConfig(resolved.root, resolved.cache)
@@ -331,13 +349,6 @@ export class Vitest {
331349

332350
this.isFirstRun = false
333351

334-
// add previously failed files
335-
// if (RERUN_FAILED) {
336-
// ctx.state.getFiles().forEach((file) => {
337-
// if (file.result?.state === 'fail')
338-
// changedTests.add(file.filepath)
339-
// })
340-
// }
341352
this.snapshot.clear()
342353
const files = Array.from(this.changedTests)
343354
this.changedTests.clear()
@@ -523,7 +534,7 @@ export class Vitest {
523534
return code.includes('import.meta.vitest')
524535
}
525536

526-
onServerRestarted(fn: () => void) {
537+
onServerRestart(fn: OnServerRestartHandler) {
527538
this._onRestartListeners.push(fn)
528539
}
529540
}

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

-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import { CSSEnablerPlugin } from './cssEnabler'
1111
import { CoverageTransform } from './coverageTransform'
1212

1313
export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()): Promise<VitePlugin[]> {
14-
let haveStarted = false
15-
1614
async function UIPlugin() {
1715
await ensurePackageInstalled('@vitest/ui', ctx.config?.root || options.root || process.cwd())
1816
return (await import('@vitest/ui')).default(options.uiBase)
@@ -145,11 +143,8 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest())
145143
process.env[name] ??= envs[name]
146144
},
147145
async configureServer(server) {
148-
if (haveStarted)
149-
await ctx.report('onServerRestart')
150146
try {
151147
await ctx.setServer(options, server)
152-
haveStarted = true
153148
if (options.api && options.watch)
154149
(await import('../../api/setup')).setup(ctx)
155150
}

‎packages/vitest/src/node/reporters/base.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,12 @@ export abstract class BaseReporter implements Reporter {
180180
return true
181181
}
182182

183-
onServerRestart() {
184-
this.ctx.logger.log(c.cyan('Restarted due to config changes...'))
183+
onServerRestart(reason?: string) {
184+
this.ctx.logger.log(c.bold(c.magenta(
185+
reason === 'config'
186+
? '\nRestarting due to config changes...'
187+
: '\nRestarting Vitest...',
188+
)))
185189
}
186190

187191
async reportSummary(files: File[]) {

‎packages/vitest/src/types/general.ts

+2
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ export interface ModuleGraphData {
7979
externalized: string[]
8080
inlined: string[]
8181
}
82+
83+
export type OnServerRestartHandler = (reason?: string) => Promise<void> | void

‎packages/vitest/src/types/reporter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface Reporter {
1313
onWatcherStart?: () => Awaitable<void>
1414
onWatcherRerun?: (files: string[], trigger?: string) => Awaitable<void>
1515

16-
onServerRestart?: () => Awaitable<void>
16+
onServerRestart?: (reason?: string) => Awaitable<void>
1717

1818
onUserConsoleLog?: (log: UserConsoleLog) => Awaitable<void>
1919
}

‎packages/vitest/src/utils/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isPackageExists } from 'local-pkg'
55
import { relative as relativeNode } from 'pathe'
66
import type { ModuleCacheMap } from 'vite-node'
77
import type { Suite, Task } from '../types'
8+
import { EXIT_CODE_RESTART } from '../constants'
89
import { getNames } from './tasks'
910

1011
export * from './tasks'
@@ -87,7 +88,7 @@ export async function ensurePackageInstalled(
8788
await (await import('@antfu/install-pkg')).installPackage(dependency, { dev: true })
8889
// TODO: somehow it fails to load the package after installation, remove this when it's fixed
8990
process.stderr.write(c.yellow(`\nPackage ${dependency} installed, re-run the command to start.\n`))
90-
process.exit(1)
91+
process.exit(EXIT_CODE_RESTART)
9192
return true
9293
}
9394

0 commit comments

Comments
 (0)
Please sign in to comment.