Skip to content
Merged
187 changes: 150 additions & 37 deletions packages/vitest/src/node/reporters/blob.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { File } from '@vitest/runner'
import type { SerializedError } from '@vitest/utils'
import type { DevEnvironment, EnvironmentModuleNode } from 'vite'
import type { Vitest } from '../core'
import type { TestProject } from '../project'
import type { Reporter } from '../types/reporter'
Expand Down Expand Up @@ -54,27 +55,40 @@ export class BlobReporter implements Reporter {
: '.vitest-reports/blob.json'
}

const modules = this.ctx.projects.map<MergeReportModuleKeys>(
(project) => {
return [
project.name,
[...project.vite.moduleGraph.idToModuleMap.entries()].map<SerializedModuleNode | null>((mod) => {
if (!mod[1].file) {
return null
}
return [mod[0], mod[1].file, mod[1].url]
}).filter(x => x != null),
]
},
)
const environmentModules: MergeReportEnvironmentModules = {}
this.ctx.projects.forEach((project) => {
const serializedProject: MergeReportEnvironmentModules[string] = {
environments: {},
external: [],
}
Object.entries(project.vite.environments).forEach(([environmentName, environment]) => {
serializedProject.environments[environmentName] = serializeEnvironmentModuleGraph(
environment,
)
})

if (project.browser?.vite.environments.client) {
serializedProject.browser = serializeEnvironmentModuleGraph(
project.browser.vite.environments.client,
)
}

for (const [id, value] of project._resolver.externalizeCache.entries()) {
if (typeof value === 'string') {
serializedProject.external.push([id, value])
}
}

environmentModules[project.name] = serializedProject
})

const report = [
this.ctx.version,
files,
errors,
modules,
coverage,
executionTime,
environmentModules,
] satisfies MergeReport

const reportFile = resolve(this.ctx.config.root, outputFile)
Expand Down Expand Up @@ -112,15 +126,15 @@ export async function readBlobs(
)
}
const content = await readFile(fullPath, 'utf-8')
const [version, files, errors, moduleKeys, coverage, executionTime] = parse(
const [version, files, errors, coverage, executionTime, environmentModules] = parse(
content,
) as MergeReport
if (!version) {
throw new TypeError(
`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`,
)
}
return { version, files, errors, moduleKeys, coverage, file: filename, executionTime }
return { version, files, errors, coverage, file: filename, executionTime, environmentModules }
})
const blobs = await Promise.all(promises)

Expand All @@ -143,28 +157,38 @@ export async function readBlobs(
)
}

// fake module graph - it is used to check if module is imported, but we don't use values inside
// Restore module graph
const projects = Object.fromEntries(
projectsArray.map(p => [p.name, p]),
)

for (const project of projectsArray) {
if (project.isBrowserEnabled()) {
await project._initBrowserServer()
}
}

blobs.forEach((blob) => {
blob.moduleKeys.forEach(([projectName, moduleIds]) => {
Object.entries(blob.environmentModules).forEach(([projectName, modulesByProject]) => {
const project = projects[projectName]
if (!project) {
return
}
moduleIds.forEach(([moduleId, file, url]) => {
const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file)
moduleNode.url = url
moduleNode.id = moduleId
moduleNode.transformResult = {
// print error checks that transformResult is set
code: ' ',
map: null,
}
project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode)

modulesByProject.external.forEach(([id, externalized]) => {
project._resolver.externalizeCache.set(id, externalized)
})

Object.entries(modulesByProject.environments).forEach(([environmentName, moduleGraph]) => {
const environment = project.vite.environments[environmentName]
deserializeEnvironmentModuleGraph(environment, moduleGraph)
})

const browserModuleGraph = modulesByProject.browser
if (browserModuleGraph) {
const browserEnvironment = project.browser!.vite.environments.client
deserializeEnvironmentModuleGraph(browserEnvironment, browserModuleGraph)
}
})
})

Expand Down Expand Up @@ -198,18 +222,107 @@ type MergeReport = [
vitestVersion: string,
files: File[],
errors: unknown[],
modules: MergeReportModuleKeys[],
coverage: unknown,
executionTime: number,
environmentModules: MergeReportEnvironmentModules,
]

type SerializedModuleNode = [
id: string,
file: string,
url: string,
]
interface MergeReportEnvironmentModules {
[projectName: string]: {
environments: {
[environmentName: string]: SerializedEnvironmentModuleGraph
}
browser?: SerializedEnvironmentModuleGraph
external: [id: string, externalized: string][]
}
}

type MergeReportModuleKeys = [
projectName: string,
modules: SerializedModuleNode[],
type SerializedEnvironmentModuleNode = [
id: number,
file: number,
url: number,
importedIds: number[],
]

interface SerializedEnvironmentModuleGraph {
idTable: string[]
modules: SerializedEnvironmentModuleNode[]
}

function serializeEnvironmentModuleGraph(
environment: DevEnvironment,
): SerializedEnvironmentModuleGraph {
const idTable: string[] = []
const idMap = new Map<string, number>()

const getIdIndex = (id: string) => {
const existing = idMap.get(id)
if (existing != null) {
return existing
}
const next = idTable.length
idMap.set(id, next)
idTable.push(id)
return next
}

const modules: SerializedEnvironmentModuleNode[] = []
for (const [id, mod] of environment.moduleGraph.idToModuleMap.entries()) {
if (!mod.file) {
continue
}

const importedIds: number[] = []
for (const importedNode of mod.importedModules) {
if (importedNode.id) {
importedIds.push(getIdIndex(importedNode.id))
}
}

modules.push([
getIdIndex(id),
getIdIndex(mod.file),
getIdIndex(mod.url),
importedIds,
])
}

return {
idTable,
modules,
}
}

function deserializeEnvironmentModuleGraph(
environment: DevEnvironment,
serialized: SerializedEnvironmentModuleGraph,
): void {
const nodesById = new Map<string, EnvironmentModuleNode>()

serialized.modules.forEach(([id, file, url]) => {
const moduleId = serialized.idTable[id]
const filePath = serialized.idTable[file]
const urlPath = serialized.idTable[url]
const moduleNode = environment.moduleGraph.createFileOnlyEntry(filePath)
moduleNode.url = urlPath
moduleNode.id = moduleId
moduleNode.transformResult = {
// print error checks that transformResult is set
code: ' ',
map: null,
}
environment.moduleGraph.idToModuleMap.set(moduleId, moduleNode)
nodesById.set(moduleId, moduleNode)
})

serialized.modules.forEach(([id, _file, _url, importedIds]) => {
const moduleId = serialized.idTable[id]
const moduleNode = nodesById.get(moduleId)!
importedIds.forEach((importedIdIndex) => {
const importedId = serialized.idTable[importedIdIndex]
const importedNode = nodesById.get(importedId)!
moduleNode.importedModules.add(importedNode)
importedNode.importers.add(moduleNode)
})
})
}
2 changes: 1 addition & 1 deletion packages/vitest/src/node/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isWindows } from '../utils/env'
export class VitestResolver {
public readonly options: ExternalizeOptions
private externalizeConcurrentCache = new Map<string, Promise<string | false | undefined>>()
private externalizeCache = new Map<string, string | false | undefined>()
public externalizeCache: Map<string, string | false | undefined> = new Map()

constructor(cacheDir: string, config: ResolvedConfig) {
// sorting to make cache consistent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test, expect } from 'vitest'
import { formatHello } from './sub/format'
import { hello } from './util'

test('passes', () => {
expect(hello()).toBe('Hello, graph!')
expect(formatHello()).toBe('Hello, graph!')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test, expect } from 'vitest'
import { hello } from './util'
import * as obug from "obug"

test('also passes', () => {
expect(hello()).toBe('Hello, graph!')
})

test('external', () => {
expect(obug).toBeTypeOf('object')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getSubject } from './subject'

export function formatHello() {
return `Hello, ${getSubject()}!`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getSubject() {
return 'graph'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getSubject } from './sub/subject'

export function hello() {
return `Hello, ${getSubject()}!`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "vitest/config"

export default defineConfig({})
Loading
Loading