Skip to content

Commit c2cceeb

Browse files
authoredJan 12, 2024
feat(vitest): allow overiding package installer with public API (#4936)
1 parent 463bee3 commit c2cceeb

File tree

14 files changed

+96
-87
lines changed

14 files changed

+96
-87
lines changed
 

‎packages/browser/src/node/providers/playwright.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
2727
return playwrightBrowsers
2828
}
2929

30-
async initialize(ctx: WorkspaceProject, { browser, options }: PlaywrightProviderOptions) {
31-
this.ctx = ctx
30+
initialize(project: WorkspaceProject, { browser, options }: PlaywrightProviderOptions) {
31+
this.ctx = project
3232
this.browser = browser
3333
this.options = options as any
3434
}

‎packages/vitest/src/integrations/browser.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { ensurePackageInstalled } from '../node/pkg'
1+
import type { WorkspaceProject } from '../node/workspace'
22
import type { BrowserProviderModule, ResolvedBrowserOptions } from '../types/browser'
33

4-
interface Loader {
5-
root: string
6-
executeId: (id: string) => any
7-
}
8-
94
const builtinProviders = ['webdriverio', 'playwright', 'none']
105

11-
export async function getBrowserProvider(options: ResolvedBrowserOptions, loader: Loader): Promise<BrowserProviderModule> {
6+
export async function getBrowserProvider(options: ResolvedBrowserOptions, project: WorkspaceProject): Promise<BrowserProviderModule> {
127
if (options.provider == null || builtinProviders.includes(options.provider)) {
13-
await ensurePackageInstalled('@vitest/browser', loader.root)
14-
const providers = await loader.executeId('@vitest/browser/providers') as {
8+
await project.ctx.packageInstaller.ensureInstalled('@vitest/browser', project.config.root)
9+
const providers = await project.runner.executeId('@vitest/browser/providers') as {
1510
webdriverio: BrowserProviderModule
1611
playwright: BrowserProviderModule
1712
none: BrowserProviderModule
@@ -23,7 +18,7 @@ export async function getBrowserProvider(options: ResolvedBrowserOptions, loader
2318
let customProviderModule
2419

2520
try {
26-
customProviderModule = await loader.executeId(options.provider) as { default: BrowserProviderModule }
21+
customProviderModule = await project.runner.executeId(options.provider) as { default: BrowserProviderModule }
2722
}
2823
catch (error) {
2924
throw new Error(`Failed to load custom BrowserProvider from ${options.provider}`, { cause: error })

‎packages/vitest/src/integrations/browser/server.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createServer } from 'vite'
22
import { defaultBrowserPort } from '../../constants'
3-
import { ensurePackageInstalled } from '../../node/pkg'
43
import { resolveApiServerConfig } from '../../node/config'
54
import { CoverageTransform } from '../../node/plugins/coverageTransform'
65
import type { WorkspaceProject } from '../../node/workspace'
@@ -10,7 +9,7 @@ import { resolveFsAllow } from '../../node/plugins/utils'
109
export async function createBrowserServer(project: WorkspaceProject, configFile: string | undefined) {
1110
const root = project.config.root
1211

13-
await ensurePackageInstalled('@vitest/browser', root)
12+
await project.ctx.packageInstaller.ensureInstalled('@vitest/browser', root)
1413

1514
const configPath = typeof configFile === 'string' ? configFile : false
1615

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { EXIT_CODE_RESTART } from '../constants'
44
import { CoverageProviderMap } from '../integrations/coverage'
55
import { getEnvPackageName } from '../integrations/env'
66
import type { UserConfig, Vitest, VitestRunMode } from '../types'
7-
import { ensurePackageInstalled } from './pkg'
87
import { createVitest } from './create'
98
import { registerConsoleShortcuts } from './stdin'
9+
import type { VitestOptions } from './core'
1010

1111
export interface CliOptions extends UserConfig {
1212
/**
@@ -25,6 +25,7 @@ export async function startVitest(
2525
cliFilters: string[] = [],
2626
options: CliOptions = {},
2727
viteOverrides?: ViteUserConfig,
28+
vitestOptions?: VitestOptions,
2829
): Promise<Vitest | undefined> {
2930
process.env.TEST = 'true'
3031
process.env.VITEST = 'true'
@@ -60,14 +61,14 @@ export async function startVitest(
6061
options.typecheck.enabled = true
6162
}
6263

63-
const ctx = await createVitest(mode, options, viteOverrides)
64+
const ctx = await createVitest(mode, options, viteOverrides, vitestOptions)
6465

6566
if (mode === 'test' && ctx.config.coverage.enabled) {
6667
const provider = ctx.config.coverage.provider || 'v8'
6768
const requiredPackages = CoverageProviderMap[provider]
6869

6970
if (requiredPackages) {
70-
if (!await ensurePackageInstalled(requiredPackages, root)) {
71+
if (!await ctx.packageInstaller.ensureInstalled(requiredPackages, root)) {
7172
process.exitCode = 1
7273
return ctx
7374
}
@@ -76,7 +77,7 @@ export async function startVitest(
7677

7778
const environmentPackage = getEnvPackageName(ctx.config.environment)
7879

79-
if (environmentPackage && !await ensurePackageInstalled(environmentPackage, root)) {
80+
if (environmentPackage && !await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) {
8081
process.exitCode = 1
8182
return ctx
8283
}

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ import { resolveConfig } from './config'
2323
import { Logger } from './logger'
2424
import { VitestCache } from './cache'
2525
import { WorkspaceProject, initializeProject } from './workspace'
26+
import { VitestPackageInstaller } from './packageInstaller'
2627

2728
const WATCHER_DEBOUNCE = 100
2829

30+
export interface VitestOptions {
31+
packageInstaller?: VitestPackageInstaller
32+
}
33+
2934
export class Vitest {
3035
config: ResolvedConfig = undefined!
3136
configOverride: Partial<ResolvedConfig> = {}
@@ -53,6 +58,8 @@ export class Vitest {
5358
restartsCount = 0
5459
runner: ViteNodeRunner = undefined!
5560

61+
public packageInstaller: VitestPackageInstaller
62+
5663
private coreWorkspaceProject!: WorkspaceProject
5764

5865
private resolvedProjects: WorkspaceProject[] = []
@@ -63,8 +70,10 @@ export class Vitest {
6370

6471
constructor(
6572
public readonly mode: VitestRunMode,
73+
options: VitestOptions = {},
6674
) {
6775
this.logger = new Logger(this)
76+
this.packageInstaller = options.packageInstaller || new VitestPackageInstaller()
6877
}
6978

7079
private _onRestartListeners: OnServerRestartHandler[] = []
@@ -139,7 +148,7 @@ export class Vitest {
139148

140149
this.reporters = resolved.mode === 'benchmark'
141150
? await createBenchmarkReporters(toArray(resolved.benchmark?.reporters), this.runner)
142-
: await createReporters(resolved.reporters, this.runner)
151+
: await createReporters(resolved.reporters, this)
143152

144153
this.cache.results.setConfig(resolved.root, resolved.cache)
145154
try {

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import type { InlineConfig as ViteInlineConfig, UserConfig as ViteUserConfig } f
44
import { findUp } from 'find-up'
55
import type { UserConfig, VitestRunMode } from '../types'
66
import { configFiles } from '../constants'
7+
import type { VitestOptions } from './core'
78
import { Vitest } from './core'
89
import { VitestPlugin } from './plugins'
910
import { createViteServer } from './vite'
1011

11-
export async function createVitest(mode: VitestRunMode, options: UserConfig, viteOverrides: ViteUserConfig = {}) {
12-
const ctx = new Vitest(mode)
12+
export async function createVitest(mode: VitestRunMode, options: UserConfig, viteOverrides: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) {
13+
const ctx = new Vitest(mode, vitestOptions)
1314
const root = resolve(options.root || process.cwd())
1415

1516
const configPath = options.config === false

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

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export { registerConsoleShortcuts } from './stdin'
77
export type { GlobalSetupContext } from './globalSetup'
88
export type { WorkspaceSpec, ProcessPool } from './pool'
99
export { createMethodsRPC } from './pools/rpc'
10+
export { VitestPackageInstaller } from './packageInstaller'
1011

1112
export type { TestSequencer, TestSequencerConstructor } from './sequencers/types'
1213
export { BaseSequencer } from './sequencers/BaseSequencer'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import url from 'node:url'
2+
import { createRequire } from 'node:module'
3+
import c from 'picocolors'
4+
import { isPackageExists } from 'local-pkg'
5+
import { EXIT_CODE_RESTART } from '../constants'
6+
import { isCI } from '../utils/env'
7+
8+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
9+
10+
export class VitestPackageInstaller {
11+
async ensureInstalled(dependency: string, root: string) {
12+
if (process.env.VITEST_SKIP_INSTALL_CHECKS)
13+
return true
14+
15+
if (process.versions.pnp) {
16+
const targetRequire = createRequire(__dirname)
17+
try {
18+
targetRequire.resolve(dependency, { paths: [root, __dirname] })
19+
return true
20+
}
21+
catch (error) {
22+
}
23+
}
24+
25+
if (isPackageExists(dependency, { paths: [root, __dirname] }))
26+
return true
27+
28+
const promptInstall = !isCI && process.stdout.isTTY
29+
30+
process.stderr.write(c.red(`${c.inverse(c.red(' MISSING DEPENDENCY '))} Cannot find dependency '${dependency}'\n\n`))
31+
32+
if (!promptInstall)
33+
return false
34+
35+
const prompts = await import('prompts')
36+
const { install } = await prompts.prompt({
37+
type: 'confirm',
38+
name: 'install',
39+
message: c.reset(`Do you want to install ${c.green(dependency)}?`),
40+
})
41+
42+
if (install) {
43+
await (await import('@antfu/install-pkg')).installPackage(dependency, { dev: true })
44+
// TODO: somehow it fails to load the package after installation, remove this when it's fixed
45+
process.stderr.write(c.yellow(`\nPackage ${dependency} installed, re-run the command to start.\n`))
46+
process.exit(EXIT_CODE_RESTART)
47+
return true
48+
}
49+
50+
return false
51+
}
52+
}

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

-50
This file was deleted.

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { relative } from 'pathe'
33
import { configDefaults } from '../../defaults'
44
import type { ResolvedConfig, UserConfig } from '../../types'
55
import { deepMerge, notNullish, removeUndefinedValues, toArray } from '../../utils'
6-
import { ensurePackageInstalled } from '../pkg'
76
import { resolveApiServerConfig } from '../config'
87
import { Vitest } from '../core'
98
import { generateScopedClassName } from '../../integrations/css/css-modules'
@@ -22,7 +21,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
2221
const getRoot = () => ctx.config?.root || options.root || process.cwd()
2322

2423
async function UIPlugin() {
25-
await ensurePackageInstalled('@vitest/ui', getRoot())
24+
await ctx.packageInstaller.ensureInstalled('@vitest/ui', getRoot())
2625
return (await import('@vitest/ui')).default(ctx)
2726
}
2827

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ViteNodeRunner } from 'vite-node/client'
2-
import type { Reporter } from '../../types'
3-
import { ensurePackageInstalled } from '../pkg'
2+
import type { Reporter, Vitest } from '../../types'
43
import { BenchmarkReportsMap, ReportersMap } from './index'
54
import type { BenchmarkBuiltinReporters, BuiltinReporters } from './index'
65

@@ -19,11 +18,12 @@ async function loadCustomReporterModule<C extends Reporter>(path: string, runner
1918
return customReporterModule.default
2019
}
2120

22-
function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, runner: ViteNodeRunner) {
21+
function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, ctx: Vitest) {
22+
const runner = ctx.runner
2323
const promisedReporters = reporterReferences.map(async (referenceOrInstance) => {
2424
if (typeof referenceOrInstance === 'string') {
2525
if (referenceOrInstance === 'html') {
26-
await ensurePackageInstalled('@vitest/ui', runner.root)
26+
await ctx.packageInstaller.ensureInstalled('@vitest/ui', runner.root)
2727
const CustomReporter = await loadCustomReporterModule('@vitest/ui/reporter', runner)
2828
return new CustomReporter()
2929
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export class WorkspaceProject {
386386
return
387387
if (this.browserProvider)
388388
return
389-
const Provider = await getBrowserProvider(this.config.browser, this.runner)
389+
const Provider = await getBrowserProvider(this.config.browser, this)
390390
this.browserProvider = new Provider()
391391
const browser = this.config.browser.name
392392
const supportedBrowsers = this.browserProvider.getSupportedBrowsers()

‎packages/vitest/src/typecheck/typechecker.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { basename, extname, resolve } from 'pathe'
66
import { TraceMap, generatedPositionFor } from '@vitest/utils/source-map'
77
import type { RawSourceMap } from '@ampproject/remapping'
88
import { getTasks } from '../utils'
9-
import { ensurePackageInstalled } from '../node/pkg'
10-
import type { Awaitable, File, ParsedStack, Task, TaskResultPack, TaskState, TscErrorInfo } from '../types'
9+
import type { Awaitable, File, ParsedStack, Task, TaskResultPack, TaskState, TscErrorInfo, Vitest } from '../types'
1110
import type { WorkspaceProject } from '../node/workspace'
1211
import { getRawErrsMapFromTsCompile, getTsconfig } from './parse'
1312
import { createIndexMap } from './utils'
@@ -225,16 +224,15 @@ export class Typechecker {
225224
this.process?.kill()
226225
}
227226

228-
protected async ensurePackageInstalled(root: string, checker: string) {
227+
protected async ensurePackageInstalled(ctx: Vitest, checker: string) {
229228
if (checker !== 'tsc' && checker !== 'vue-tsc')
230229
return
231230
const packageName = checker === 'tsc' ? 'typescript' : 'vue-tsc'
232-
await ensurePackageInstalled(packageName, root)
231+
await ctx.packageInstaller.ensureInstalled(packageName, ctx.config.root)
233232
}
234233

235234
public async prepare() {
236235
const { root, typecheck } = this.ctx.config
237-
await this.ensurePackageInstalled(root, typecheck.checker)
238236

239237
const { config, path } = await getTsconfig(root, typecheck)
240238

‎test/reporters/tests/utils.test.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import { resolve } from 'pathe'
55
import type { ViteNodeRunner } from 'vite-node/client'
6+
import type { Vitest } from 'vitest'
67
import { describe, expect, test } from 'vitest'
78
import { createReporters } from '../../../packages/vitest/src/node/reporters/utils'
89
import { DefaultReporter } from '../../../packages/vitest/src/node/reporters/default'
@@ -12,29 +13,32 @@ const customReporterPath = resolve(__dirname, '../src/custom-reporter.js')
1213
const fetchModule = {
1314
executeId: (id: string) => import(id),
1415
} as ViteNodeRunner
16+
const ctx = {
17+
runner: fetchModule,
18+
} as Vitest
1519

1620
describe('Reporter Utils', () => {
1721
test('passing an empty array returns nothing', async () => {
18-
const promisedReporters = await createReporters([], fetchModule)
22+
const promisedReporters = await createReporters([], ctx)
1923
expect(promisedReporters).toHaveLength(0)
2024
})
2125

2226
test('passing the name of a single built-in reporter returns a new instance', async () => {
23-
const promisedReporters = await createReporters(['default'], fetchModule)
27+
const promisedReporters = await createReporters(['default'], ctx)
2428
expect(promisedReporters).toHaveLength(1)
2529
const reporter = promisedReporters[0]
2630
expect(reporter).toBeInstanceOf(DefaultReporter)
2731
})
2832

2933
test('passing in the path to a custom reporter returns a new instance', async () => {
30-
const promisedReporters = await createReporters(([customReporterPath]), fetchModule)
34+
const promisedReporters = await createReporters(([customReporterPath]), ctx)
3135
expect(promisedReporters).toHaveLength(1)
3236
const customReporter = promisedReporters[0]
3337
expect(customReporter).toBeInstanceOf(TestReporter)
3438
})
3539

3640
test('passing in a mix of built-in and custom reporters works', async () => {
37-
const promisedReporters = await createReporters(['default', customReporterPath], fetchModule)
41+
const promisedReporters = await createReporters(['default', customReporterPath], ctx)
3842
expect(promisedReporters).toHaveLength(2)
3943
const defaultReporter = promisedReporters[0]
4044
expect(defaultReporter).toBeInstanceOf(DefaultReporter)

0 commit comments

Comments
 (0)
Please sign in to comment.