Skip to content

Commit c2cceeb

Browse files
authored
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 5 additions & 10 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 5 additions & 4 deletions
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

Lines changed: 10 additions & 1 deletion
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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'
Lines changed: 52 additions & 0 deletions
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

Lines changed: 0 additions & 50 deletions
This file was deleted.

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

Lines changed: 1 addition & 2 deletions
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

0 commit comments

Comments
 (0)