Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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