Skip to content

Commit 780b187

Browse files
authoredApr 23, 2024··
feat(api): startVitest() to accept stdout and stdin (#5493)
1 parent 23bd3cd commit 780b187

34 files changed

+375
-350
lines changed
 

‎eslint.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export default antfu(
1313
'**/assets/**',
1414
'**/*.timestamp-*',
1515
'test/core/src/self',
16+
'test/cache/cache/.vitest-base/results.json',
1617
'test/wasm-modules/src/wasm-bindgen-no-cyclic',
1718
'test/workspaces/results.json',
19+
'test/workspaces-browser/results.json',
1820
'test/reporters/fixtures/with-syntax-error.test.js',
1921
'test/network-imports/public/slash@3.0.0.js',
2022
'test/coverage-test/src/transpiled.js',

‎packages/vitest/rollup.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const external = [
6565
'worker_threads',
6666
'node:worker_threads',
6767
'node:fs',
68+
'node:stream',
6869
'node:vm',
6970
'inspector',
7071
'vite-node/source-map',

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ export async function startVitest(
7373
return ctx
7474
}
7575

76+
const stdin = vitestOptions?.stdin || process.stdin
77+
const stdout = vitestOptions?.stdout || process.stdout
7678
let stdinCleanup
77-
if (process.stdin.isTTY && ctx.config.watch)
78-
stdinCleanup = registerConsoleShortcuts(ctx)
79+
if (stdin.isTTY && ctx.config.watch)
80+
stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout)
7981

8082
ctx.onServerRestart((reason) => {
8183
ctx.report('onServerRestart', reason)

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { BaseSequencer } from './sequencers/BaseSequencer'
1212
import { RandomSequencer } from './sequencers/RandomSequencer'
1313
import type { BenchmarkBuiltinReporters } from './reporters'
1414
import { builtinPools } from './pool'
15+
import type { Logger } from './logger'
1516

1617
function resolvePath(path: string, root: string) {
1718
return normalize(
@@ -76,13 +77,14 @@ export function resolveConfig(
7677
mode: VitestRunMode,
7778
options: UserConfig,
7879
viteConfig: ResolvedViteConfig,
80+
logger: Logger,
7981
): ResolvedConfig {
8082
if (options.dom) {
8183
if (
8284
viteConfig.test?.environment != null
8385
&& viteConfig.test!.environment !== 'happy-dom'
8486
) {
85-
console.warn(
87+
logger.console.warn(
8688
c.yellow(
8789
`${c.inverse(c.yellow(' Vitest '))} Your config.test.environment ("${
8890
viteConfig.test.environment
@@ -209,11 +211,11 @@ export function resolveConfig(
209211
return
210212

211213
if (option === 'fallbackCJS') {
212-
console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`))
214+
logger.console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`))
213215
}
214216
else {
215217
const transformMode = resolved.environment === 'happy-dom' || resolved.environment === 'jsdom' ? 'web' : 'ssr'
216-
console.warn(
218+
logger.console.warn(
217219
c.yellow(
218220
`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. If you rely on vite-node directly, use "server.deps.${option}" instead. Otherwise, consider using "deps.optimizer.${transformMode}.${option === 'external' ? 'exclude' : 'include'}"`,
219221
),
@@ -475,7 +477,7 @@ export function resolveConfig(
475477
let cacheDir = VitestCache.resolveCacheDir('', resolve(viteConfig.cacheDir, 'vitest'), resolved.name)
476478

477479
if (resolved.cache && resolved.cache.dir) {
478-
console.warn(
480+
logger.console.warn(
479481
c.yellow(
480482
`${c.inverse(c.yellow(' Vitest '))} "cache.dir" is deprecated, use Vite's "cacheDir" instead if you want to change the cache director. Note caches will be written to "cacheDir\/vitest"`,
481483
),
@@ -514,7 +516,7 @@ export function resolveConfig(
514516
resolved.typecheck.enabled ??= false
515517

516518
if (resolved.typecheck.enabled)
517-
console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow SemVer, please pin Vitest\'s version when using it.'))
519+
logger.console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow SemVer, please pin Vitest\'s version when using it.'))
518520

519521
resolved.browser ??= {} as any
520522
resolved.browser.enabled ??= false

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { existsSync, promises as fs } from 'node:fs'
2+
import type { Writable } from 'node:stream'
23
import { isMainThread } from 'node:worker_threads'
34
import type { ViteDevServer } from 'vite'
45
import { mergeConfig } from 'vite'
@@ -31,6 +32,9 @@ const WATCHER_DEBOUNCE = 100
3132

3233
export interface VitestOptions {
3334
packageInstaller?: VitestPackageInstaller
35+
stdin?: NodeJS.ReadStream
36+
stdout?: NodeJS.WriteStream | Writable
37+
stderr?: NodeJS.WriteStream | Writable
3438
}
3539

3640
export class Vitest {
@@ -74,7 +78,7 @@ export class Vitest {
7478
public readonly mode: VitestRunMode,
7579
options: VitestOptions = {},
7680
) {
77-
this.logger = new Logger(this)
81+
this.logger = new Logger(this, options.stdout, options.stderr)
7882
this.packageInstaller = options.packageInstaller || new VitestPackageInstaller()
7983
}
8084

@@ -93,7 +97,7 @@ export class Vitest {
9397
this.runningPromise = undefined
9498
this.projectsTestFiles.clear()
9599

96-
const resolved = resolveConfig(this.mode, options, server.config)
100+
const resolved = resolveConfig(this.mode, options, server.config, this.logger)
97101

98102
this.server = server
99103
this.config = resolved

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Console } from 'node:console'
2+
import type { Writable } from 'node:stream'
13
import { createLogUpdate } from 'log-update'
24
import c from 'picocolors'
35
import { version } from '../../../../package.json'
@@ -24,17 +26,19 @@ const CURSOR_TO_START = `${ESC}1;1H`
2426
const CLEAR_SCREEN = '\x1Bc'
2527

2628
export class Logger {
27-
outputStream = process.stdout
28-
errorStream = process.stderr
29-
logUpdate = createLogUpdate(process.stdout)
29+
logUpdate: ReturnType<typeof createLogUpdate>
3030

3131
private _clearScreenPending: string | undefined
3232
private _highlights = new Map<string, string>()
33+
public console: Console
3334

3435
constructor(
3536
public ctx: Vitest,
36-
public console = globalThis.console,
37+
public outputStream: NodeJS.WriteStream | Writable = process.stdout,
38+
public errorStream: NodeJS.WriteStream | Writable = process.stderr,
3739
) {
40+
this.console = new Console({ stdout: outputStream, stderr: errorStream })
41+
this.logUpdate = createLogUpdate(this.outputStream)
3842
this._highlights.clear()
3943
}
4044

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ export abstract class BaseReporter implements Reporter {
191191
return
192192
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined
193193
const header = c.gray(log.type + c.dim(` | ${task ? getFullName(task, c.dim(' > ')) : log.taskId !== UNKNOWN_TEST_ID ? log.taskId : 'unknown test'}`))
194-
process[log.type].write(`${header}\n${log.content}\n`)
194+
195+
const output = log.type === 'stdout' ? this.ctx.logger.outputStream : this.ctx.logger.errorStream
196+
197+
// @ts-expect-error -- write() method has different signature on the union type
198+
output.write(`${header}\n${log.content}\n`)
195199
}
196200

197201
shouldLog(log: UserConsoleLog) {

‎packages/vitest/src/node/reporters/github-actions.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Console } from 'node:console'
21
import { Writable } from 'node:stream'
32
import { getTasks } from '@vitest/runner/utils'
43
import stripAnsi from 'strip-ansi'
@@ -76,7 +75,7 @@ async function printErrorWrapper(error: unknown, ctx: Vitest, project: Workspace
7675
})
7776
const result = await printError(error, project, {
7877
showCodeFrame: false,
79-
logger: new Logger(ctx, new Console(writable, writable)),
78+
logger: new Logger(ctx, writable, writable),
8079
})
8180
return { nearest: result?.nearest, output }
8281
}

‎packages/vitest/src/node/reporters/renderers/dotRenderer.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ export function createDotRenderer(_tasks: Task[], options: DotRendererOptions) {
9292
let timer: any
9393

9494
const { logUpdate: log, outputStream } = options.logger
95+
const columns = 'columns' in outputStream ? outputStream.columns : 80
9596

9697
function update() {
97-
log(render(tasks, outputStream.columns))
98+
log(render(tasks, columns))
9899
}
99100

100101
return {
@@ -114,7 +115,7 @@ export function createDotRenderer(_tasks: Task[], options: DotRendererOptions) {
114115
timer = undefined
115116
}
116117
log.clear()
117-
options.logger.log(render(tasks, outputStream.columns))
118+
options.logger.log(render(tasks, columns))
118119
return this
119120
},
120121
clear() {

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

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import readline from 'node:readline'
2+
import type { Writable } from 'node:stream'
23
import c from 'picocolors'
34
import prompt from 'prompts'
45
import { relative } from 'pathe'
@@ -28,7 +29,7 @@ ${keys.map(i => c.dim(' press ') + c.reset([i[0]].flat().map(c.bold).join(', ')
2829
)
2930
}
3031

31-
export function registerConsoleShortcuts(ctx: Vitest) {
32+
export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable) {
3233
let latestFilename = ''
3334

3435
async function _keypressHandler(str: string, key: any) {
@@ -97,7 +98,7 @@ export function registerConsoleShortcuts(ctx: Vitest) {
9798

9899
async function inputNamePattern() {
99100
off()
100-
const watchFilter = new WatchFilter('Input test name pattern (RegExp)')
101+
const watchFilter = new WatchFilter('Input test name pattern (RegExp)', stdin, stdout)
101102
const filter = await watchFilter.filter((str: string) => {
102103
const files = ctx.state.getFiles()
103104
const tests = getTests(files)
@@ -130,7 +131,7 @@ export function registerConsoleShortcuts(ctx: Vitest) {
130131
async function inputFilePattern() {
131132
off()
132133

133-
const watchFilter = new WatchFilter('Input filename pattern')
134+
const watchFilter = new WatchFilter('Input filename pattern', stdin, stdout)
134135

135136
const filter = await watchFilter.filter(async (str: string) => {
136137
const files = await ctx.globTestFiles([str])
@@ -149,19 +150,19 @@ export function registerConsoleShortcuts(ctx: Vitest) {
149150
let rl: readline.Interface | undefined
150151
function on() {
151152
off()
152-
rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 })
153-
readline.emitKeypressEvents(process.stdin, rl)
154-
if (process.stdin.isTTY)
155-
process.stdin.setRawMode(true)
156-
process.stdin.on('keypress', keypressHandler)
153+
rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 })
154+
readline.emitKeypressEvents(stdin, rl)
155+
if (stdin.isTTY)
156+
stdin.setRawMode(true)
157+
stdin.on('keypress', keypressHandler)
157158
}
158159

159160
function off() {
160161
rl?.close()
161162
rl = undefined
162-
process.stdin.removeListener('keypress', keypressHandler)
163-
if (process.stdin.isTTY)
164-
process.stdin.setRawMode(false)
163+
stdin.removeListener('keypress', keypressHandler)
164+
if (stdin.isTTY)
165+
stdin.setRawMode(false)
165166
}
166167

167168
on()

‎packages/vitest/src/node/watch-filter.ts

+33-19
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import readline from 'node:readline'
2+
import type { Writable } from 'node:stream'
23
import c from 'picocolors'
34
import stripAnsi from 'strip-ansi'
45
import { createDefer } from '@vitest/utils'
5-
import { stdout } from '../utils'
6+
import { stdout as getStdout } from '../utils'
67

78
const MAX_RESULT_COUNT = 10
89
const SELECTION_MAX_INDEX = 7
@@ -17,24 +18,29 @@ export class WatchFilter {
1718
private results: string[] = []
1819
private selectionIndex = -1
1920
private onKeyPress?: (str: string, key: any) => void
21+
private stdin: NodeJS.ReadStream
22+
private stdout: NodeJS.WriteStream | Writable
2023

21-
constructor(message: string) {
24+
constructor(message: string, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable = getStdout()) {
2225
this.message = message
23-
this.filterRL = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 })
24-
readline.emitKeypressEvents(process.stdin, this.filterRL)
25-
if (process.stdin.isTTY)
26-
process.stdin.setRawMode(true)
26+
this.stdin = stdin
27+
this.stdout = stdout
28+
29+
this.filterRL = readline.createInterface({ input: this.stdin, escapeCodeTimeout: 50 })
30+
readline.emitKeypressEvents(this.stdin, this.filterRL)
31+
if (this.stdin.isTTY)
32+
this.stdin.setRawMode(true)
2733
}
2834

2935
public async filter(filterFunc: FilterFunc): Promise<string | undefined> {
30-
stdout().write(this.promptLine())
36+
this.write(this.promptLine())
3137

3238
const resultPromise = createDefer<string | undefined>()
3339

3440
this.onKeyPress = this.filterHandler(filterFunc, (result) => {
3541
resultPromise.resolve(result)
3642
})
37-
process.stdin.on('keypress', this.onKeyPress)
43+
this.stdin.on('keypress', this.onKeyPress)
3844
try {
3945
return await resultPromise
4046
}
@@ -138,31 +144,39 @@ export class WatchFilter {
138144
private eraseAndPrint(str: string) {
139145
let rows = 0
140146
const lines = str.split(/\r?\n/)
141-
for (const line of lines)
147+
for (const line of lines) {
148+
const columns = 'columns' in this.stdout ? this.stdout.columns : 80
149+
142150
// We have to take care of screen width in case of long lines
143-
rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / stdout().columns)
151+
rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / columns)
152+
}
144153

145-
stdout().write(`${ESC}1G`) // move to the beginning of the line
146-
stdout().write(`${ESC}J`) // erase down
147-
stdout().write(str)
148-
stdout().write(`${ESC}${rows - 1}A`) // moving up lines
154+
this.write(`${ESC}1G`) // move to the beginning of the line
155+
this.write(`${ESC}J`) // erase down
156+
this.write(str)
157+
this.write(`${ESC}${rows - 1}A`) // moving up lines
149158
}
150159

151160
private close() {
152161
this.filterRL.close()
153162
if (this.onKeyPress)
154-
process.stdin.removeListener('keypress', this.onKeyPress)
163+
this.stdin.removeListener('keypress', this.onKeyPress)
155164

156-
if (process.stdin.isTTY)
157-
process.stdin.setRawMode(false)
165+
if (this.stdin.isTTY)
166+
this.stdin.setRawMode(false)
158167
}
159168

160169
private restoreCursor() {
161170
const cursortPos = this.keywordOffset() + (this.currentKeyword?.length || 0)
162-
stdout().write(`${ESC}${cursortPos}G`)
171+
this.write(`${ESC}${cursortPos}G`)
163172
}
164173

165174
private cancel() {
166-
stdout().write(`${ESC}J`) // erase down
175+
this.write(`${ESC}J`) // erase down
176+
}
177+
178+
private write(data: string) {
179+
// @ts-expect-error -- write() method has different signature on the union type
180+
this.stdout.write(data)
167181
}
168182
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ export class WorkspaceProject {
308308
coverage: this.ctx.config.coverage,
309309
},
310310
server.config,
311+
this.ctx.logger,
311312
)
312313

313314
this.server = server

0 commit comments

Comments
 (0)
Please sign in to comment.