UNPKG

vite

Version:

Native-ESM powered web dev build tool

201 lines (184 loc) 6.21 kB
import { extname } from 'path' import { isDirectCSSRequest } from '../plugins/css' import { cleanUrl, normalizePath, removeImportQuery, removeTimestampQuery } from '../utils' import { FS_PREFIX } from '../constants' import { TransformResult } from './transformRequest' import { PluginContainer } from './pluginContainer' import { parse as parseUrl } from 'url' export class ModuleNode { /** * Public served url path, starts with / */ url: string /** * Resolved file system path + query */ id: string | null = null file: string | null = null type: 'js' | 'css' importers = new Set<ModuleNode>() importedModules = new Set<ModuleNode>() acceptedHmrDeps = new Set<ModuleNode>() isSelfAccepting = false transformResult: TransformResult | null = null ssrTransformResult: TransformResult | null = null ssrModule: Record<string, any> | null = null lastHMRTimestamp = 0 constructor(url: string) { this.url = url this.type = isDirectCSSRequest(url) ? 'css' : 'js' } } function invalidateSSRModule(mod: ModuleNode, seen: Set<ModuleNode>) { if (seen.has(mod)) { return } seen.add(mod) mod.ssrModule = null mod.importers.forEach((importer) => invalidateSSRModule(importer, seen)) } export class ModuleGraph { urlToModuleMap = new Map<string, ModuleNode>() idToModuleMap = new Map<string, ModuleNode>() // a single file may corresponds to multiple modules with different queries fileToModulesMap = new Map<string, Set<ModuleNode>>() safeModulesPath = new Set<string>() container: PluginContainer constructor(container: PluginContainer) { this.container = container } async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> { const [url] = await this.resolveUrl(rawUrl) return this.urlToModuleMap.get(url) } getModuleById(id: string): ModuleNode | undefined { return this.idToModuleMap.get(removeTimestampQuery(id)) } getModulesByFile(file: string): Set<ModuleNode> | undefined { return this.fileToModulesMap.get(file) } onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { const seen = new Set<ModuleNode>() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) } } invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void { mod.transformResult = null mod.ssrTransformResult = null invalidateSSRModule(mod, seen) } invalidateAll(): void { const seen = new Set<ModuleNode>() this.idToModuleMap.forEach((mod) => { this.invalidateModule(mod, seen) }) } /** * Update the module graph based on a module's updated imports information * If there are dependencies that no longer have any importers, they are * returned as a Set. */ async updateModuleInfo( mod: ModuleNode, importedModules: Set<string | ModuleNode>, acceptedModules: Set<string | ModuleNode>, isSelfAccepting: boolean ): Promise<Set<ModuleNode> | undefined> { mod.isSelfAccepting = isSelfAccepting const prevImports = mod.importedModules const nextImports = (mod.importedModules = new Set()) let noLongerImported: Set<ModuleNode> | undefined // update import graph for (const imported of importedModules) { const dep = typeof imported === 'string' ? await this.ensureEntryFromUrl(imported) : imported dep.importers.add(mod) nextImports.add(dep) } // remove the importer from deps that were imported but no longer are. prevImports.forEach((dep) => { if (!nextImports.has(dep)) { dep.importers.delete(mod) if (!dep.importers.size) { // dependency no longer imported ;(noLongerImported || (noLongerImported = new Set())).add(dep) } } }) // update accepted hmr deps const deps = (mod.acceptedHmrDeps = new Set()) for (const accepted of acceptedModules) { const dep = typeof accepted === 'string' ? await this.ensureEntryFromUrl(accepted) : accepted deps.add(dep) } return noLongerImported } async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> { const [url, resolvedId] = await this.resolveUrl(rawUrl) let mod = this.urlToModuleMap.get(url) if (!mod) { mod = new ModuleNode(url) this.urlToModuleMap.set(url, mod) mod.id = resolvedId this.idToModuleMap.set(resolvedId, mod) const file = (mod.file = cleanUrl(resolvedId)) let fileMappedModules = this.fileToModulesMap.get(file) if (!fileMappedModules) { fileMappedModules = new Set() this.fileToModulesMap.set(file, fileMappedModules) } fileMappedModules.add(mod) } return mod } // some deps, like a css file referenced via @import, don't have its own // url because they are inlined into the main css import. But they still // need to be represented in the module graph so that they can trigger // hmr in the importing css file. createFileOnlyEntry(file: string): ModuleNode { file = normalizePath(file) let fileMappedModules = this.fileToModulesMap.get(file) if (!fileMappedModules) { fileMappedModules = new Set() this.fileToModulesMap.set(file, fileMappedModules) } const url = `${FS_PREFIX}${file}` for (const m of fileMappedModules) { if (m.url === url || m.id === file) { return m } } const mod = new ModuleNode(url) mod.file = file fileMappedModules.add(mod) return mod } // for incoming urls, it is important to: // 1. remove the HMR timestamp query (?t=xxxx) // 2. resolve its extension so that urls with or without extension all map to // the same module async resolveUrl(url: string): Promise<[string, string]> { url = removeImportQuery(removeTimestampQuery(url)) const resolvedId = (await this.container.resolveId(url))?.id || url const ext = extname(cleanUrl(resolvedId)) const { pathname, search, hash } = parseUrl(url) if (ext && !pathname!.endsWith(ext)) { url = pathname + ext + (search || '') + (hash || '') } return [url, resolvedId] } }