Skip to content
Closed
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
14 changes: 10 additions & 4 deletions packages/ui/client/composables/client/static.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ModuleGraphData, RunnerTestFile, SerializedRootConfig } from 'vitest'
import type { RunnerTestFile, SerializedRootConfig } from 'vitest'
import type { SerializedProjectModules } from '../../../../vitest/src/utils/module-graph/types'
import type { VitestClient, VitestClientRpc } from './ws'
import { decompressSync, strFromU8 } from 'fflate'
import { parse } from 'flatted'
import { reactive } from 'vue'
import { getSerializedTestFileModuleGraph } from '../../../../vitest/src/utils/module-graph/serialized'
import { StateManager } from './state'

export interface HTMLReportMetadata {
files: RunnerTestFile[]
config: SerializedRootConfig
moduleGraph: Record<string, Record<string, ModuleGraphData>>
environmentModules: Record<string, SerializedProjectModules>
unhandledErrors: unknown[]
testModules: {
projectName: string
Expand Down Expand Up @@ -38,8 +40,12 @@ function deserializeReportMetadata(metadata: HTMLReportMetadata) {
getConfig: async () => {
return metadata.config
},
getModuleGraph: async (projectName, id) => {
return metadata.moduleGraph[projectName]?.[id]
getModuleGraph: async (projectName, id, viteEnvironment) => {
return getSerializedTestFileModuleGraph(
metadata.environmentModules[projectName],
id,
viteEnvironment,
)
},
getUnhandledErrors: async () => {
return metadata.unhandledErrors
Expand Down
24 changes: 5 additions & 19 deletions packages/ui/node/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { gzip, constants as zlibConstants } from 'node:zlib'
import { stringify } from 'flatted'
import { dirname, relative, resolve } from 'pathe'
import c from 'tinyrainbow'
import { getModuleGraph } from '../../vitest/src/utils/graph'
import { serializeProjectModules } from '../../vitest/src/node/module-graph'
import { distClientRoot } from './paths'

const gzipAsync = promisify(gzip)
Expand Down Expand Up @@ -120,7 +120,7 @@ async function serializeReportMetadata(
files: [],
config: ctx.serializedRootConfig,
unhandledErrors: [...unhandledErrors],
moduleGraph: {},
environmentModules: {},
testModules: [],
sourceCode: {
codeTable: [],
Expand All @@ -146,8 +146,9 @@ async function serializeReportMetadata(
result.sourceCode.codeTable.push(code)
return index
}

const promises: Promise<void>[] = []
ctx.projects.forEach((project) => {
result.environmentModules[project.name] = serializeProjectModules(project)
})

for (const testModule of testModules) {
result.files.push(testModule.task)
Expand All @@ -167,23 +168,8 @@ async function serializeReportMetadata(
testModuleCodes[projectName][testModule.relativeModuleId] = getCodeIndex(code)
} catch {}
}

// TODO: https://github.com/vitest-dev/vitest/issues/9763
promises.push(
(async () => {
result.moduleGraph[projectName] ??= {}
result.moduleGraph[projectName][testModule.moduleId] = await getModuleGraph(
ctx,
projectName,
testModule.moduleId,
testModule.viteEnvironment?.name,
)
})(),
)
}

await Promise.all(promises)

return result
}

Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { WebSocketServer } from 'ws'
import { API_PATH } from '../constants'
import { getModuleGraph } from '../node/module-graph'
import { isFileServingAllowed } from '../node/vite'
import { getTestFileEnvironment } from '../utils/environments'
import { getModuleGraph } from '../utils/graph'
import { stringifyReplace } from '../utils/serialization'
import { isValidApiRequest } from './check'

Expand Down
159 changes: 159 additions & 0 deletions packages/vitest/src/node/module-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { DevEnvironment, EnvironmentModuleNode } from 'vite'
import type { ModuleGraphData } from '../types/general'
import type {
SerializedEnvironmentModuleGraph,
SerializedEnvironmentModuleNode,
SerializedProjectModules,
} from '../utils/module-graph/types'
import type { Vitest } from './core'
import type { TestProject } from './project'
import { getTestFileEnvironment } from '../utils/environments'
import { getTestFileModuleGraph } from '../utils/module-graph/query'

export async function getModuleGraph(
ctx: Vitest,
projectName: string,
testFilePath: string,
viteEnvironment?: string,
): Promise<ModuleGraphData> {
const project = ctx.getProjectByName(projectName)
const browser = project.config.browser.enabled

let environment: DevEnvironment | undefined

if (viteEnvironment) {
environment = project.vite.environments[viteEnvironment]
} else {
environment =
project.config.experimental.viteModuleRunner === false
? project.vite.environments.__vitest__
: getTestFileEnvironment(project, testFilePath, browser)
}

if (!environment) {
throw new Error(`Cannot find environment for ${testFilePath}`)
}
return getTestFileModuleGraph(project, environment, testFilePath)
}

export function serializeProjectModules(project: TestProject): SerializedProjectModules {
const serialized: SerializedProjectModules = {
environments: {},
external: [],
project: {
setupFiles: project.config.setupFiles,
},
}

Object.entries(project.vite.environments).forEach(([environmentName, environment]) => {
serialized.environments[environmentName] = serializeEnvironmentModuleGraph(environment)
})

if (project.config.experimental.viteModuleRunner === false) {
serialized.project.defaultEnvironment = '__vitest__'
} else if (project.config.browser.enabled) {
serialized.project.defaultEnvironment = 'client'
serialized.project.browserCacheDir = project.browser?.vite.config.cacheDir
}

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

return serialized
}

export function restoreProjectModules(
project: TestProject,
serialized: SerializedProjectModules,
): void {
serialized.external.forEach(([id, externalized]) => {
project._resolver.externalizeCache.set(id, externalized)
})

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

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()) {
// Vite can generate module with `file = ""` for module id "#..."
// when the actual module doesn't exist (e.g. resolve failure or mocked module)
if (mod.file == null) {
continue
}

const importedIds: number[] = []
for (const importedNode of mod.importedModules) {
if (importedNode.id !== null) {
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]
// `createFileOnlyEntry('')` normalizes the file to ".". This keeps
// the graph usable, but doesn't perfectly round-trip Vite's `file = ""`
// nodes for ids like "#...".
// We may just do moduleNode.file = filePath in the future.
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)
})
})
}
Loading
Loading