vite
Version:
Native-ESM powered web dev build tool
201 lines (184 loc) • 6.21 kB
text/typescript
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]
}
}