import type { Rollup, ResolvedConfig as ViteConfig, ViteDevServer } from 'vite' import type { ServerIdResolution, ServerMockResolution } from '../types' import { existsSync, readFileSync } from 'node:fs' import { isAbsolute, join, resolve } from 'pathe' import { cleanUrl } from '../utils' import { findMockRedirect } from './redirect' export interface ServerResolverOptions { /** * @default ['/node_modules/'] */ moduleDirectories?: string[] } const VALID_ID_PREFIX = '/@id/' export class ServerMockResolver { constructor( private server: ViteDevServer, private options: ServerResolverOptions = {}, ) {} async resolveMock( rawId: string, importer: string, options: { mock: 'spy' | 'factory' | 'auto' }, ): Promise { const { id, fsPath, external } = await this.resolveMockId(rawId, importer) const resolvedUrl = this.normalizeResolveIdToUrl({ id }).url if (options.mock === 'factory') { const manifest = getViteDepsManifest(this.server.config) const needsInterop = manifest?.[fsPath]?.needsInterop ?? false return { mockType: 'manual', resolvedId: id, resolvedUrl, needsInterop } } if (options.mock === 'spy') { return { mockType: 'autospy', resolvedId: id, resolvedUrl } } const redirectUrl = findMockRedirect(this.server.config.root, fsPath, external) return { mockType: redirectUrl === null ? 'automock' : 'redirect', redirectUrl, resolvedId: id, resolvedUrl, } } public invalidate(ids: string[]): void { ids.forEach((id) => { const moduleGraph = this.server.moduleGraph const module = moduleGraph.getModuleById(id) if (module) { module.transformResult = null } }) } public async resolveId(id: string, importer?: string): Promise { const resolved = await this.server.environments.client.pluginContainer.resolveId(id, importer) if (!resolved) { return null } return this.normalizeResolveIdToUrl(resolved) } private normalizeResolveIdToUrl(resolved: { id: string }) { const isOptimized = resolved.id.startsWith(withTrailingSlash(this.server.config.cacheDir)) let url: string // normalise the URL to be acceptable by the browser // https://github.com/vitejs/vite/blob/14027b0f2a9b01c14815c38aab22baf5b29594bb/packages/vite/src/node/plugins/importAnalysis.ts#L103 const root = this.server.config.root if (resolved.id.startsWith(withTrailingSlash(root))) { url = resolved.id.slice(root.length) } else if ( resolved.id !== '/@react-refresh' && isAbsolute(resolved.id) && existsSync(cleanUrl(resolved.id)) ) { url = join('/@fs/', resolved.id) } else { url = resolved.id } if (url[0] !== '.' && url[0] !== '/') { url = resolved.id.startsWith(VALID_ID_PREFIX) ? resolved.id : VALID_ID_PREFIX + resolved.id.replace('\0', '__x00__') } return { id: resolved.id, url, optimized: isOptimized, } } private async resolveMockId(rawId: string, importer: string) { if ( !this.server.moduleGraph.getModuleById(importer) && !importer.startsWith(this.server.config.root) ) { importer = join(this.server.config.root, importer) } const resolved = await this.server.pluginContainer.resolveId(rawId, importer, { ssr: false, }) return this.resolveModule(rawId, resolved) } private resolveModule(rawId: string, resolved: Rollup.PartialResolvedId | null) { const id = resolved?.id || rawId const external = !isAbsolute(id) || isModuleDirectory(this.options, id) ? rawId : null return { id, fsPath: cleanUrl(id), external, } } } function isModuleDirectory(config: ServerResolverOptions, path: string) { const moduleDirectories = config.moduleDirectories || ['/node_modules/'] return moduleDirectories.some((dir: string) => path.includes(dir)) } interface PartialManifest { [name: string]: { hash: string needsInterop: boolean } } const metadata = new WeakMap() function getViteDepsManifest(config: ViteConfig) { if (metadata.has(config)) { return metadata.get(config)! } const cacheDirPath = getDepsCacheDir(config) const metadataPath = resolve(cacheDirPath, '_metadata.json') if (!existsSync(metadataPath)) { return null } const { optimized } = JSON.parse(readFileSync(metadataPath, 'utf-8')) const newManifest: PartialManifest = {} for (const name in optimized) { const dep = optimized[name] const file = resolve(cacheDirPath, dep.file) newManifest[file] = { hash: dep.fileHash, needsInterop: dep.needsInterop, } } metadata.set(config, newManifest) return newManifest } function getDepsCacheDir(config: ViteConfig): string { return resolve(config.cacheDir, 'deps') } function withTrailingSlash(path: string): string { if (path.at(-1) !== '/') { return `${path}/` } return path }