From 121b161fa81ab06331553f4c017cb357a1d0822d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 18 Oct 2024 10:26:20 +0200 Subject: [PATCH] fix(vitest): print warnings form Vite plugins (#6724) --- packages/browser/src/node/index.ts | 4 +- packages/browser/src/node/plugin.ts | 34 +++-- packages/vitest/src/node/create.ts | 1 - packages/vitest/src/node/packageInstaller.ts | 4 + packages/vitest/src/node/plugins/index.ts | 9 ++ packages/vitest/src/node/plugins/utils.ts | 2 +- packages/vitest/src/node/plugins/workspace.ts | 8 + packages/vitest/src/node/vite.ts | 5 +- packages/vitest/src/node/viteLogger.ts | 137 ++++++++++++++++++ packages/vitest/src/node/workspace.ts | 1 - packages/vitest/src/public/node.ts | 2 +- test/coverage-test/utils.ts | 5 +- 12 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 packages/vitest/src/node/viteLogger.ts diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 5130cc4baaa6..87cd060c0d05 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -32,7 +32,9 @@ export async function createBrowserServer( const logLevel = (process.env.VITEST_BROWSER_DEBUG as 'info') ?? 'info' - const logger = createViteLogger(logLevel) + const logger = createViteLogger(project.logger, logLevel, { + allowClearScreen: false, + }) const vite = await createViteServer({ ...project.options, // spread project config inlined in root workspace config diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index 9b0957f3ba21..b5121f3e0536 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url' import { createRequire } from 'node:module' import { lstatSync, readFileSync } from 'node:fs' import type { Stats } from 'node:fs' -import { basename, extname, resolve } from 'pathe' +import { basename, dirname, extname, resolve } from 'pathe' import sirv from 'sirv' import type { WorkspaceProject } from 'vitest/node' import { getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir as vitestDist } from 'vitest/node' @@ -23,6 +23,12 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { const distRoot = resolve(pkgRoot, 'dist') const project = browserServer.project + function isPackageExists(pkg: string, root: string) { + return browserServer.project.ctx.packageInstaller.isPackageExists?.(pkg, { + paths: [root], + }) + } + return [ { enforce: 'pre', @@ -211,14 +217,14 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { const coverage = project.ctx.config.coverage const provider = coverage.provider if (provider === 'v8') { - const path = tryResolve('@vitest/coverage-v8', [project.ctx.config.root]) + const path = tryResolve('@vitest/coverage-v8', [project.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-v8/browser') } } else if (provider === 'istanbul') { - const path = tryResolve('@vitest/coverage-istanbul', [project.ctx.config.root]) + const path = tryResolve('@vitest/coverage-istanbul', [project.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-istanbul') @@ -239,18 +245,18 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { '@vitest/browser > @testing-library/dom', ] - const react = tryResolve('vitest-browser-react', [project.ctx.config.root]) - if (react) { - include.push(react) - } - const vue = tryResolve('vitest-browser-vue', [project.ctx.config.root]) - if (vue) { - include.push(vue) - } + const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root - const svelte = tryResolve('vitest-browser-svelte', [project.ctx.config.root]) + const svelte = isPackageExists('vitest-browser-svelte', fileRoot) if (svelte) { - exclude.push(svelte) + exclude.push('vitest-browser-svelte') + } + + // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it + // have ?. until Vitest 3.0 for backwards compatibility + const vueTestUtils = isPackageExists('@vue/test-utils', fileRoot) + if (vueTestUtils) { + include.push('@vue/test-utils') } return { @@ -398,7 +404,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { { name: 'test-utils-rewrite', setup(build) { - build.onResolve({ filter: /@vue\/test-utils/ }, (args) => { + build.onResolve({ filter: /^@vue\/test-utils$/ }, (args) => { const _require = getRequire() // resolve to CJS instead of the browser because the browser version expects a global Vue object const resolved = _require.resolve(args.path, { diff --git a/packages/vitest/src/node/create.ts b/packages/vitest/src/node/create.ts index 2c7dc0a2545a..bc45a5e02da3 100644 --- a/packages/vitest/src/node/create.ts +++ b/packages/vitest/src/node/create.ts @@ -31,7 +31,6 @@ export async function createVitest( options.config = configPath const config: ViteInlineConfig = { - logLevel: 'error', configFile: configPath, // this will make "mode": "test" | "benchmark" inside defineConfig mode: options.mode || mode, diff --git a/packages/vitest/src/node/packageInstaller.ts b/packages/vitest/src/node/packageInstaller.ts index b0f6f3d21fae..1b59f43dc685 100644 --- a/packages/vitest/src/node/packageInstaller.ts +++ b/packages/vitest/src/node/packageInstaller.ts @@ -7,6 +7,10 @@ import { isCI } from '../utils/env' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) export class VitestPackageInstaller { + isPackageExists(name: string, options?: { paths?: string[] }) { + return isPackageExists(name, options) + } + async ensureInstalled(dependency: string, root: string, version?: string) { if (process.env.VITEST_SKIP_INSTALL_CHECKS) { return true diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index f9bae896b898..6eec9cc1830f 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -11,6 +11,7 @@ import { resolveApiServerConfig } from '../config/resolveConfig' import { Vitest } from '../core' import { generateScopedClassName } from '../../integrations/css/css-modules' import { defaultPort } from '../../constants' +import { createViteLogger } from '../viteLogger' import { SsrReplacerPlugin } from './ssrReplacer' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' @@ -132,6 +133,14 @@ export async function VitestPlugin( }, } + config.customLogger = createViteLogger( + ctx.logger, + viteConfig.logLevel || 'warn', + { + allowClearScreen: false, + }, + ) + // If "coverage.exclude" is not defined by user, add "test.include" to "coverage.exclude" automatically if (userConfig.coverage?.enabled && !userConfig.coverage.exclude && userConfig.include && config.test) { config.test.coverage = { diff --git a/packages/vitest/src/node/plugins/utils.ts b/packages/vitest/src/node/plugins/utils.ts index 63edb936c915..2ebbd3fd4848 100644 --- a/packages/vitest/src/node/plugins/utils.ts +++ b/packages/vitest/src/node/plugins/utils.ts @@ -75,7 +75,7 @@ export function resolveOptimizerConfig( // `optimizeDeps.disabled` is deprecated since v5.1.0-beta.1 // https://github.com/vitejs/vite/pull/15184 - if (major >= 5 && minor >= 1) { + if ((major >= 5 && minor >= 1) || major >= 6) { if (newConfig.optimizeDeps.disabled) { newConfig.optimizeDeps.noDiscovery = true newConfig.optimizeDeps.include = [] diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 92afa6dfb87d..87240eea6fe9 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -6,6 +6,7 @@ import { configDefaults } from '../../defaults' import { generateScopedClassName } from '../../integrations/css/css-modules' import type { WorkspaceProject } from '../workspace' import type { ResolvedConfig, UserWorkspaceConfig } from '../types/config' +import { createViteLogger } from '../viteLogger' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { SsrReplacerPlugin } from './ssrReplacer' @@ -124,6 +125,13 @@ export function WorkspaceVitestPlugin( } } } + config.customLogger = createViteLogger( + project.logger, + viteConfig.logLevel || 'warn', + { + allowClearScreen: false, + }, + ) return config }, diff --git a/packages/vitest/src/node/vite.ts b/packages/vitest/src/node/vite.ts index bb3b294f673b..2464de8aeb88 100644 --- a/packages/vitest/src/node/vite.ts +++ b/packages/vitest/src/node/vite.ts @@ -15,10 +15,7 @@ export async function createViteServer(inlineConfig: InlineConfig) { error(...args) } - const server = await createServer({ - logLevel: 'error', - ...inlineConfig, - }) + const server = await createServer(inlineConfig) console.error = error return server diff --git a/packages/vitest/src/node/viteLogger.ts b/packages/vitest/src/node/viteLogger.ts new file mode 100644 index 000000000000..c351afeccebe --- /dev/null +++ b/packages/vitest/src/node/viteLogger.ts @@ -0,0 +1,137 @@ +import type { LogErrorOptions, LogLevel, LogType, Logger, LoggerOptions } from 'vite' +import type { RollupError } from 'rollup' +import colors from 'tinyrainbow' +import type { Logger as VitestLogger } from './logger' + +const LogLevels: Record = { + silent: 0, + error: 1, + warn: 2, + info: 3, +} + +function clearScreen(logger: VitestLogger) { + const repeatCount = process.stdout.rows - 2 + const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : '' + logger.clearScreen(blank) +} + +let lastType: LogType | undefined +let lastMsg: string | undefined +let sameCount = 0 + +// Only initialize the timeFormatter when the timestamp option is used, and +// reuse it across all loggers +let timeFormatter: Intl.DateTimeFormat +function getTimeFormatter() { + timeFormatter ??= new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }) + return timeFormatter +} + +// This is copy-pasted and needs to be synced from time to time. Ideally, Vite's `createLogger` should accept a custom `console` +// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/logger.ts?rgh-link-date=2024-10-16T23%3A29%3A19Z +// When Vitest supports only Vite 6 and above, we can use Vite's `createLogger({ console })` +// https://github.com/vitejs/vite/pull/18379 +export function createViteLogger( + console: VitestLogger, + level: LogLevel = 'info', + options: LoggerOptions = {}, +): Logger { + const loggedErrors = new WeakSet() + const { prefix = '[vite]', allowClearScreen = true } = options + const thresh = LogLevels[level] + const canClearScreen + = allowClearScreen && process.stdout.isTTY && !process.env.CI + const clear = canClearScreen ? clearScreen : () => {} + + function format(type: LogType, msg: string, options: LogErrorOptions = {}) { + if (options.timestamp) { + let tag = '' + if (type === 'info') { + tag = colors.cyan(colors.bold(prefix)) + } + else if (type === 'warn') { + tag = colors.yellow(colors.bold(prefix)) + } + else { + tag = colors.red(colors.bold(prefix)) + } + const environment = (options as any).environment ? `${(options as any).environment} ` : '' + return `${colors.dim(getTimeFormatter().format(new Date()))} ${tag} ${environment}${msg}` + } + else { + return msg + } + } + + function output(type: LogType, msg: string, options: LogErrorOptions = {}) { + if (thresh >= LogLevels[type]) { + const method = type === 'info' ? 'log' : type + + if (options.error) { + loggedErrors.add(options.error) + } + if (canClearScreen) { + if (type === lastType && msg === lastMsg) { + sameCount++ + clear(console) + console[method]( + format(type, msg, options), + colors.yellow(`(x${sameCount + 1})`), + ) + } + else { + sameCount = 0 + lastMsg = msg + lastType = type + if (options.clear) { + clear(console) + } + console[method](format(type, msg, options)) + } + } + else { + console[method](format(type, msg, options)) + } + } + } + + const warnedMessages = new Set() + + const logger: Logger = { + hasWarned: false, + info(msg, opts) { + output('info', msg, opts) + }, + warn(msg, opts) { + logger.hasWarned = true + output('warn', msg, opts) + }, + warnOnce(msg, opts) { + if (warnedMessages.has(msg)) { + return + } + logger.hasWarned = true + output('warn', msg, opts) + warnedMessages.add(msg) + }, + error(msg, opts) { + logger.hasWarned = true + output('error', msg, opts) + }, + clearScreen(type) { + if (thresh >= LogLevels[type]) { + clear(console) + } + }, + hasErrorLogged(error) { + return loggedErrors.has(error) + }, + } + + return logger +} diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 3186b9d46189..42e8787faec3 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -71,7 +71,6 @@ export async function initializeProject( const config: ViteInlineConfig = { ...options, root, - logLevel: 'error', configFile, // this will make "mode": "test" | "benchmark" inside defineConfig mode: options.test?.mode || options.mode || ctx.config.mode, diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 37a408fd6f83..dd123c44f336 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -18,6 +18,7 @@ export { createDebugger } from '../utils/debugger' export { resolveFsAllow } from '../node/plugins/utils' export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig' export { TestSpecification } from '../node/spec' +export { createViteLogger } from '../node/viteLogger' export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors' @@ -54,7 +55,6 @@ export { isFileServingAllowed, parseAst, parseAstAsync, - createLogger as createViteLogger, } from 'vite' /** @deprecated use `createViteServer` instead */ export const createServer = _createServer diff --git a/test/coverage-test/utils.ts b/test/coverage-test/utils.ts index f9eac91cc78d..e448875ec401 100644 --- a/test/coverage-test/utils.ts +++ b/test/coverage-test/utils.ts @@ -5,8 +5,9 @@ import { stripVTControlCharacters } from 'node:util' import { normalize } from 'pathe' import libCoverage from 'istanbul-lib-coverage' import type { FileCoverageData } from 'istanbul-lib-coverage' -import type { TestFunction, UserConfig } from 'vitest' +import type { TestFunction } from 'vitest' import { vi, describe as vitestDescribe, test as vitestTest } from 'vitest' +import type { UserConfig } from 'vitest/node' import * as testUtils from '../test-utils' export function test(name: string, fn: TestFunction, skip = false) { @@ -55,7 +56,7 @@ export async function runVitest(config: UserConfig, options = { throwOnError: tr if (options.throwOnError) { if (result.stderr !== '') { - throw new Error(result.stderr) + throw new Error(`stderr:\n${result.stderr}\n\nstdout:\n${result.stdout}`) } }