vitest relatedはVitestの命令の1つです。指定したファイルに関連するテストを自動で実行してくれます。Jestでも–findRelatedTestsというオプションで同様の動作ができます。
これは特定のファイルの依存を色々確認できるということです。どうやって依存を取得しているかを深ぼれば面白そうだったので、調べてみました。
とりあえず上記のサブコマンドのエントリーポイントまで愚直に見ていきます。
まずは package.json
の bin
を見ると vitest.mjs
となっています。
"bin": {
"vitest": "./vitest.mjs"
},
vitest.mjs
はシンプルに ./dist/cli.js
を読み込んでいます。
#!/usr/bin/env node
import './dist/cli.js'
vitestの設定を見るとrollup
を使ってビルドをしているので、rollup.config.js
を確認すると、おそらくsrc/node/cli.ts
が実際の処理になっていそうです。
'cli': 'src/node/cli.ts',
で、最終的にsrc/node/cli/cac.ts
に行き着きます。
import { createCLI } from './cli/cac'
createCLI().parse()
cacはCLIアプリのフレームワーク的なものですね。
related
のサブコマンドの定義は runRelated
を呼び出す様になっており。
cli
.command('related [...filters]', undefined, options)
.action(runRelated)
runRelated
は下記のような定義です。
async function runRelated(relatedFiles: string[] | string, argv: CliOptions): Promise<void> {
argv.related = relatedFiles
argv.passWithNoTests ??= true
await start('test', [], argv)
}
つまりは、 argv.related
にファイル情報を入れている状態で、 start
関数を呼び出している事がわかります。start
も同ファイルに定義されており、src/node/cli/cli-api.ts
のstartVitest
関数を呼び出す様になっています。
async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<void> {
try {
process.title = 'node (vitest)'
}
catch {}
try {
const { startVitest } = await import('./cli-api')
const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(cliFilters, options))
if (!ctx.shouldKeepServer()) {
await ctx.exit()
}
}
catch (e) {
const { divider } = await import('../reporters/renderers/utils')
console.error(`\n${c.red(divider(c.bold(c.inverse(' Startup Error '))))}`)
console.error(e)
console.error('\n\n')
if (process.exitCode == null) {
process.exitCode = 1
}
process.exit()
}
}
上記のファイルを追いかけると最終的に src/node/core.ts
にたどり着きます。名前からしても重要そうですね。
start
といういかにもなメソッドがあるためそれを読んでいきます。おそらく対象のファイルを取得する処理があります。this.specifications.getRelevantTestSpecifications
です。
async start(filters?: string[]): Promise<TestRunResult> {
try {
await this.initCoverageProvider()
await this.coverageProvider?.clean(this.config.coverage.clean)
}
finally {
await this.report('onInit', this)
}
this.filenamePattern = filters && filters?.length > 0 ? filters : undefined
const files = await this.specifications.getRelevantTestSpecifications(filters)
該当の処理はfilterTestsBySource
を呼んでいるだけです。
public async getRelevantTestSpecifications(filters: string[] = []): Promise<TestSpecification[]> {
return this.filterTestsBySource(
await this.globTestSpecifications(filters),
)
}
そちらの処理を見ると、config.related
を利用している部分があります。
private async filterTestsBySource(specs: TestSpecification[]): Promise<TestSpecification[]> {
if (this.vitest.config.changed && !this.vitest.config.related) {
const { VitestGit } = await import('./git')
const vitestGit = new VitestGit(this.vitest.config.root)
const related = await vitestGit.findChangedFiles({
changedSince: this.vitest.config.changed,
})
if (!related) {
process.exitCode = 1
throw new GitNotFoundError()
}
this.vitest.config.related = Array.from(new Set(related))
}
const related = this.vitest.config.related
if (!related) {
return specs
}
const forceRerunTriggers = this.vitest.config.forceRerunTriggers
if (forceRerunTriggers.length && mm(related, forceRerunTriggers).length) {
return specs
}
// don't run anything if no related sources are found
// if we are in watch mode, we want to process all tests
if (!this.vitest.config.watch && !related.length) {
return []
}
const testGraphs = await Promise.all(
specs.map(async (spec) => {
const deps = await this.getTestDependencies(spec)
return [spec, deps] as const
}),
)
const runningTests: TestSpecification[] = []
for (const [specification, deps] of testGraphs) {
// if deps or the test itself were changed
if (related.some(path => path === specification.moduleId || deps.has(path))) {
runningTests.push(specification)
}
}
下記の処理が関連ファイルかどうかを判定しているものっぽいですね。 getTestDependencies
て該当のファイルの依存関係を取得し、その依存関係の中に関連ファイルのファイルがあるかどうかを判定している様です。
const testGraphs = await Promise.all(
specs.map(async (spec) => {
const deps = await this.getTestDependencies(spec)
return [spec, deps] as const
}),
)
const runningTests: TestSpecification[] = []
for (const [specification, deps] of testGraphs) {
// if deps or the test itself were changed
if (related.some(path => path === specification.moduleId || deps.has(path))) {
runningTests.push(specification)
}
}
getTestDependencies
は下記のような処理です。
private async getTestDependencies(spec: TestSpecification, deps = new Set<string>()): Promise<Set<string>> {
const addImports = async (project: TestProject, filepath: string) => {
if (deps.has(filepath)) {
return
}
deps.add(filepath)
const mod = project.vite.moduleGraph.getModuleById(filepath)
const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath)
if (!transformed) {
return
}
const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []]
await Promise.all(dependencies.map(async (dep) => {
const fsPath = dep.startsWith('/@fs/')
? dep.slice(isWindows ? 5 : 4)
: join(project.config.root, dep)
if (!fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
await addImports(project, fsPath)
}
}))
}
await addImports(spec.project, spec.moduleId)
deps.delete(spec.moduleId)
return deps
}
project.vite.moduleGraph.getModuleById
と project.vitenode.transformRequest
というメソッドが気になりますね。これらのAPIを利用すれば依存関係がいい感じに取得できそうです。
まずは project.vite
を見てみます。vite
は ViteDevServer
というvite
側のインスタンスを持っているようです。Viteではおなじみの createDevServer
で得られるインスタンスですね。Viteのドキュメントでも説明されています。
moduleGraph
は下記のような説明となっています。
Module graph that tracks the import relationships, url to file mapping and hmr state.
インポート関係、urlとファイルのマッピング、hmrの状態を追跡するモジュールグラフ。
/**
* Module graph that tracks the import relationships, url to file mapping
* and hmr state.
*/
moduleGraph: ModuleGraph
まぁ名前のとおりではありますが、モジュールのグラフを追跡できるものですね。
transformRequest
を確認してみます。
検索してみると下記にたどり着きます。
export function transformRequest(
コードを読んでみると最終的にはmoduleGraph
が登場します。戻り値にdeps
やdynamicDeps
がありますしね。
まとめると、viteの内部的には該当のファイル(モジュール)の依存を追うdeps
なりdynamicDeps
という物があり、それを利用して依存しているファイルを確認し自動でテストを実行しているようです。