Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338import { performance } from 'node:perf_hooks'import { existsSync } from 'node:fs'import assert from 'node:assert'import { join, normalize, relative, resolve } from 'pathe'import type { TransformResult, ViteDevServer } from 'vite'import createDebug from 'debug'import type { DebuggerOptions, EncodedSourceMap, FetchResult, ViteNodeResolveId, ViteNodeServerOptions } from './types'import { shouldExternalize } from './externalize'import { normalizeModuleId, toArray, toFilePath, withTrailingSlash } from './utils'import { Debugger } from './debug'import { withInlineSourcemap } from './source-map'
export * from './externalize'
interface FetchCache { duration?: number timestamp: number result: FetchResult}
const debugRequest = createDebug('vite-node:server:request')
export class ViteNodeServer { private fetchPromiseMap = { ssr: new Map<string, Promise<FetchResult>>(), web: new Map<string, Promise<FetchResult>>(), }
private transformPromiseMap = { ssr: new Map<string, Promise<TransformResult | null | undefined>>(), web: new Map<string, Promise<TransformResult | null | undefined>>(), }
private durations = { ssr: new Map<string, number[]>(), web: new Map<string, number[]>(), }
private existingOptimizedDeps = new Set<string>()
fetchCaches = { ssr: new Map<string, FetchCache>(), web: new Map<string, FetchCache>(), }
fetchCache = new Map<string, FetchCache>()
externalizeCache = new Map<string, Promise<string | false>>()
debugger?: Debugger
constructor( public server: ViteDevServer, public options: ViteNodeServerOptions = {}, ) { const ssrOptions = server.config.ssr
options.deps ??= {} options.deps.cacheDir = relative(server.config.root, options.deps.cacheDir || server.config.cacheDir)
if (ssrOptions) { // we don't externalize ssr, because it has different semantics in Vite // if (ssrOptions.external) { // options.deps.external ??= [] // options.deps.external.push(...ssrOptions.external) // }
if (ssrOptions.noExternal === true) { options.deps.inline ??= true } else if (options.deps.inline !== true) { options.deps.inline ??= [] const inline = options.deps.inline options.deps.inline.push(...toArray(ssrOptions.noExternal).filter(dep => !inline.includes(dep))) } } if (process.env.VITE_NODE_DEBUG_DUMP) { options.debug = Object.assign(<DebuggerOptions>{ dumpModules: !!process.env.VITE_NODE_DEBUG_DUMP, loadDumppedModules: process.env.VITE_NODE_DEBUG_DUMP === 'load', }, options.debug ?? {}) } if (options.debug) this.debugger = new Debugger(server.config.root, options.debug!)
options.deps.moduleDirectories ??= []
const envValue = process.env.VITE_NODE_DEPS_MODULE_DIRECTORIES || process.env.npm_config_VITE_NODE_DEPS_MODULE_DIRECTORIES const customModuleDirectories = envValue?.split(',') if (customModuleDirectories) options.deps.moduleDirectories.push(...customModuleDirectories)
options.deps.moduleDirectories = options.deps.moduleDirectories.map((dir) => { if (!dir.startsWith('/')) dir = `/${dir}` if (!dir.endsWith('/')) dir += '/' return normalize(dir) })
// always add node_modules as a module directory if (!options.deps.moduleDirectories.includes('/node_modules/')) options.deps.moduleDirectories.push('/node_modules/') }
shouldExternalize(id: string) { return shouldExternalize(id, this.options.deps, this.externalizeCache) }
public getTotalDuration() { const ssrDurations = [...this.durations.ssr.values()].flat() const webDurations = [...this.durations.web.values()].flat() return [...ssrDurations, ...webDurations].reduce((a, b) => a + b, 0) }
private async ensureExists(id: string): Promise<boolean> { if (this.existingOptimizedDeps.has(id)) return true if (existsSync(id)) { this.existingOptimizedDeps.add(id) return true } return new Promise<boolean>((resolve) => { setTimeout(() => { this.ensureExists(id).then(() => { resolve(true) }) }) }) }
async resolveId(id: string, importer?: string, transformMode?: 'web' | 'ssr'): Promise<ViteNodeResolveId | null> { if (importer && !importer.startsWith(withTrailingSlash(this.server.config.root))) importer = resolve(this.server.config.root, importer) const mode = transformMode ?? ((importer && this.getTransformMode(importer)) || 'ssr') return this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' }) }
getSourceMap(source: string) { const fetchResult = this.fetchCache.get(source)?.result if (fetchResult?.map) return fetchResult.map const ssrTransformResult = this.server.moduleGraph.getModuleById(source)?.ssrTransformResult return (ssrTransformResult?.map || null) as unknown as EncodedSourceMap | null }
private assertMode(mode: 'web' | 'ssr') { assert(mode === 'web' || mode === 'ssr', `"transformMode" can only be "web" or "ssr", received "${mode}".`) }
async fetchModule(id: string, transformMode?: 'web' | 'ssr'): Promise<FetchResult> { const mode = transformMode || this.getTransformMode(id) return this.fetchResult(id, mode) .then((r) => { return this.options.sourcemap !== true ? { ...r, map: undefined } : r }) }
async fetchResult(id: string, mode: 'web' | 'ssr') { const moduleId = normalizeModuleId(id) this.assertMode(mode) const promiseMap = this.fetchPromiseMap[mode] // reuse transform for concurrent requests if (!promiseMap.has(moduleId)) { promiseMap.set(moduleId, this._fetchModule(moduleId, mode) .finally(() => { promiseMap.delete(moduleId) })) } return promiseMap.get(moduleId)! }
async transformRequest(id: string, filepath = id, transformMode?: 'web' | 'ssr') { const mode = transformMode || this.getTransformMode(id) this.assertMode(mode) const promiseMap = this.transformPromiseMap[mode] // reuse transform for concurrent requests if (!promiseMap.has(id)) { promiseMap.set(id, this._transformRequest(id, filepath, mode) .finally(() => { promiseMap.delete(id) })) } return promiseMap.get(id)! }
async transformModule(id: string, transformMode?: 'web' | 'ssr') { if (transformMode !== 'web') throw new Error('`transformModule` only supports `transformMode: "web"`.')
const normalizedId = normalizeModuleId(id) const mod = this.server.moduleGraph.getModuleById(normalizedId) const result = mod?.transformResult || await this.server.transformRequest(normalizedId)
return { code: result?.code, } }
getTransformMode(id: string) { const withoutQuery = id.split('?')[0]
if (this.options.transformMode?.web?.some(r => withoutQuery.match(r))) return 'web' if (this.options.transformMode?.ssr?.some(r => withoutQuery.match(r))) return 'ssr'
if (withoutQuery.match(/\.([cm]?[jt]sx?|json)$/)) return 'ssr' return 'web' }
private getChangedModule( id: string, file: string, ) { const module = this.server.moduleGraph.getModuleById(id) || this.server.moduleGraph.getModuleById(file) if (module) return module const _modules = this.server.moduleGraph.getModulesByFile(file) if (!_modules || !_modules.size) return null // find the latest changed module const modules = [..._modules] let mod = modules[0] let latestMax = -1 for (const m of _modules) { const timestamp = Math.max(m.lastHMRTimestamp, m.lastInvalidationTimestamp) if (timestamp > latestMax) { latestMax = timestamp mod = m } } return mod }
private async _fetchModule(id: string, transformMode: 'web' | 'ssr'): Promise<FetchResult> { let result: FetchResult
const cacheDir = this.options.deps?.cacheDir
if (cacheDir && id.includes(cacheDir)) { if (!id.startsWith(withTrailingSlash(this.server.config.root))) id = join(this.server.config.root, id) const timeout = setTimeout(() => { throw new Error(`ViteNodeServer: ${id} not found. This is a bug, please report it.`) }, 5000) // CI can be quite slow await this.ensureExists(id) clearTimeout(timeout) }
const { path: filePath } = toFilePath(id, this.server.config.root)
const moduleNode = this.getChangedModule(id, filePath) const cache = this.fetchCaches[transformMode].get(filePath)
// lastUpdateTimestamp is the timestamp that marks the last time the module was changed // if lastUpdateTimestamp is 0, then the module was not changed since the server started // we test "timestamp === 0" for expressiveness, but it's not necessary const timestamp = moduleNode ? Math.max(moduleNode.lastHMRTimestamp, moduleNode.lastInvalidationTimestamp) : 0 if (cache && (timestamp === 0 || cache.timestamp >= timestamp)) return cache.result
const time = Date.now() const externalize = await this.shouldExternalize(filePath) let duration: number | undefined if (externalize) { result = { externalize } this.debugger?.recordExternalize(id, externalize) } else { const start = performance.now() const r = await this._transformRequest(id, filePath, transformMode) duration = performance.now() - start result = { code: r?.code, map: r?.map as any } }
const cacheEntry = { duration, timestamp: time, result, }
const durations = this.durations[transformMode].get(filePath) || [] this.durations[transformMode].set( filePath, [...durations, duration ?? 0], )
this.fetchCaches[transformMode].set(filePath, cacheEntry) this.fetchCache.set(filePath, cacheEntry)
return result }
protected async processTransformResult(filepath: string, result: TransformResult) { const mod = this.server.moduleGraph.getModuleById(filepath) return withInlineSourcemap(result, { filepath: mod?.file || filepath, root: this.server.config.root, }) }
private async _transformRequest(id: string, filepath: string, transformMode: 'web' | 'ssr') { debugRequest(id)
let result: TransformResult | null = null
if (this.options.debug?.loadDumppedModules) { result = await this.debugger?.loadDump(id) ?? null if (result) return result }
if (transformMode === 'web') { // for components like Vue, we want to use the client side // plugins but then convert the code to be consumed by the server result = await this.server.transformRequest(id) if (result) result = await this.server.ssrTransform(result.code, result.map, id) } else { result = await this.server.transformRequest(id, { ssr: true }) }
const sourcemap = this.options.sourcemap ?? 'inline' if (sourcemap === 'inline' && result && !id.includes('node_modules')) result = await this.processTransformResult(filepath, result)
if (this.options.debug?.dumpModules) await this.debugger?.dumpFile(id, result)
return result }}