Every build tool has a Graph to maintain references and Module information between modules. This article examines what Vite’s Module Graph looks like

Create an instance of Module Graph

Initialization occurs during createServer

const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)

const server: ViteDevServer = {
    moduleGraph,
    // ...
}
Copy the code

Create a ModuleGraph instance and pass in the created plug-in container container

export class ModuleGraph {
    urlToModuleMap = new Map<string, ModuleNode>()
    idToModuleMap = new Map<string, ModuleNode>()
    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> {}

    getModuleById(id: string): ModuleNode | undefined {}

    getModulesByFile(file: string) :Set<ModuleNode> | undefined {}

    onFileChange(file: string) :void {}

    invalidateModule(): void {}

    invalidateAll(): void {}
    async updateModuleInfo(): Promise<Set<ModuleNode> | undefined> {}
    async ensureEntryFromUrl(rawUrl: string) :Promise<ModuleNode> {}
    createFileOnlyEntry(file: string): ModuleNode {}
    async resolveUrl(url: string) :Promise"[string.string]> {}
}
Copy the code

The initialization process is to mount the plugin container to this and initialize the urlToModuleMap, idToModuleMap, fileToModulesMap, and safeModulesPath attributes

Let’s take a look at each method separately

resolveUrl

async resolveUrl(url: string) :Promise"[string.string] > {/ / remove? The import and t = XXX
    url = removeImportQuery(removeTimestampQuery(url))
    / / here
    const resolvedId = (await this.container.resolveId(url))? .id || urlconst ext = extname(cleanUrl(resolvedId))
    const { pathname, search, hash } = parseUrl(url)
    if(ext && ! pathname! .endsWith(ext)) { url = pathname + ext + (search ||' ') + (hash || ' ')}return [url, resolvedId]
}
Copy the code

This method calls the resolveId hook function of all plug-ins, retrieves the absolute path to the file based on the url of the requested module, and returns the absolute path to the file.

ensureEntryFromUrl

async ensureEntryFromUrl(rawUrl: string) :Promise<ModuleNode> {
    // Get the file URL and absolute path
    const [url, resolvedId] = await this.resolveUrl(rawUrl)
    // Get the ModuleNode instance corresponding to the URL
    let mod = this.urlToModuleMap.get(url)
    if(! mod) {// Initializes the ModuleNode instance
        mod = new ModuleNode(url)
        // Add the mod to urlToModuleMap
        this.urlToModuleMap.set(url, mod)
        / / set id
        mod.id = resolvedId
        / / set idToModuleMap
        this.idToModuleMap.set(resolvedId, mod)
        const file = (mod.file = cleanUrl(resolvedId))
        let fileMappedModules = this.fileToModulesMap.get(file)
        if(! fileMappedModules) { fileMappedModules =new Set(a)/ / set fileToModulesMap
            this.fileToModulesMap.set(file, fileMappedModules)
        }
        fileMappedModules.add(mod)
    }
    return mod
}
Copy the code

Create ModuleNode objects based on the module path and collect them into ModuleGraph’s properties; Finally, this object is returned

  • Added to theurlToModuleMapIn, the key is fileurl; Value is corresponding to the moduleMoudleNodeobject
  • Added to theidToModuleMap, the key is the absolute file path; Value is corresponding to the moduleMoudleNodeobject
  • Added to thefileToModulesMapIn, the key is removedqueryandhashIs the absolute path of the file. The value isSetInstance, which adds the corresponding of the moduleMoudleNodeobject

Inside the object is some information about the module and the relationships between modules; Look at the object properties

- url: INDICATES the URL starting with /, for example, / SRC /assets/logo. PNG - id: indicates the absolute path of the module, which may contain query andhash- file: no query andhashModule absolute path -type: If it is a CSS file and the path has the direct parameter'css', or for'js'-lasthMRTIMESTAMP: HMR update time - a Set of modules that import the module into which the element is a ModuleNode object - importedModules: The current module imports a Set of modules Set, elements of which are ModuleNode objects - transformResult: {code: source code, map: sourcemap related, etag: Unique value, related to comparison caching} the following value is related to import.meta.hot.accept() -isselfaccepting: if the module updates itselftrue-acceptedhmrdeps: Set of modules to receive hot updates for the current module. The elements are ModuleNode objects. And the import meta. Hot. The accept ()Copy the code

Get ModuleNode

// Get the ModuleGraph object corresponding to the module based on the URL
async getModuleByUrl(rawUrl: string) :Promise<ModuleNode | undefined> {
    const [url] = await this.resolveUrl(rawUrl)
    return this.urlToModuleMap.get(url)
}
// Get the ModuleGraph object corresponding to the module based on the absolute path that may have arguments
getModuleById(id: string): ModuleNode | undefined {
    return this.idToModuleMap.get(removeTimestampQuery(id))
}
// Get the Set of ModuleGraph objects corresponding to the module based on the absolute path with no parameters
getModulesByFile(file: string) :Set<ModuleNode> | undefined {
    return this.fileToModulesMap.get(file)
}
Copy the code

Empty ModuleNode

// Clears the transformResult of the ModuleGraph object
invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()) :void {
    mod.transformResult = null
}
// Clears the transformResult of all ModuleGraph objects
invalidateAll(): void {
    const seen = new Set<ModuleNode>()
    this.idToModuleMap.forEach((mod) = > {
        this.invalidateModule(mod, seen)
    })
}
Copy the code

onFileChange

onFileChange(file: string) :void {
    const mods = this.getModulesByFile(file)
    if (mods) {
        const seen = new Set<ModuleNode>()
        mods.forEach((mod) = > {
            this.invalidateModule(mod, seen)
        })
    }
}
Copy the code

Gets and clears the transformResult property value of the corresponding ModuleNode object based on the passed file

updateModuleInfo

The most important method is to build and update reference relationships between modules

async updateModuleInfo(
    mod: ModuleNode, // ModuleNode object corresponding to the current module
    importedModules: Set<string | ModuleNode>, // The module imported by the current module
    acceptedModules: Set<string | ModuleNode>, // The current module receives a collection of hot update modules
    isSelfAccepting: boolean // True for self-updating) :Promise<Set<ModuleNode> | undefined> {
    // If true, it receives hot updates from the module itself
    mod.isSelfAccepting = isSelfAccepting
    // Import the collection before getting the module
    const prevImports = mod.importedModules
    // Create a new Set
    const nextImports = (mod.importedModules = new Set())
    let noLongerImported: Set<ModuleNode> | undefined
    // update import graph
    / / traverse importedModules
    for (const imported of importedModules) {
        // Create/find ModuleNode instances for dependent modules if imported is a string
        const dep =
            typeof imported === 'string'
                ? await this.ensureEntryFromUrl(imported)
                : imported
        // Adds the current module's ModuleNode instance to the dependent module's corresponding ModuleNode instance whose importers are the credit
        dep.importers.add(mod)
        // Add the dependent module's corresponding ModuleNode instance to nextImports
        nextImports.add(dep)
    }
    prevImports.forEach((dep) = > {
        // If nextImports does not have this dep
        // The module corresponding to the deP is not imported in the current module
        // So the mod is removed from the DEP
        if(! nextImports.has(dep)) { dep.importers.delete(mod)if(! dep.importers.size) {// If no module is imported, it will be collected in noLongerImported; (noLongerImported || (noLongerImported =new Set())).add(
                    dep
                )
            }
        }
    })
    // Add the module set in import.meta.hot.accept() to the mod.acceptedModules, excluding itself
    const deps = (mod.acceptedHmrDeps = new Set())
    for (const accepted of acceptedModules) {
        const dep =
            typeof accepted === 'string'
                ? await this.ensureEntryFromUrl(accepted)
                : accepted
        deps.add(dep)
    }
    // The current module was imported, and now no set of files imported by the module is returned
    return noLongerImported
}
Copy the code

conclusion

Vite creates a ModuleNode object for each module, which contains references between modules and module information. Module information includes the absolute path, converted code, received hot updated modules, and so on.