Meaning of Code. HMR in Vite. Part 6
Import Analysis
The import analysis plugin is added to the configuration during Create Server stage (see resolvePlugins in vite/packages/vite/src/node/plugins/index.ts which is called from resolveConfig in vite/packages/vite/src/node/config.ts). The name of this plugin does not tell much, but it plays crucial part in HMR machinery and injects all the code needed for HMR to work on client side and also analyses dependencies between modules (collects all imports). The plugin is more than 600 lines of code so I am planning to spare you and include only important parts (no need to thank me).
First lets answer the question, who adds all the accept code? And the answer is - it is not Vite. Vite only provides API for HMR. The rest should be done by the plugins, provided by the frameworks (for example see vue plugin). Plugins transform source code during script transform stage (see Script Transformation Pipeline section) and inject accept calls.
Next question is, how Vite knows, that accept is called. When injected accept method is called on the client side, it does not pass any information to the Vite server at all. And now one of the most stunning things to me, Vite understands that accept is "called" from the source code :). Vite parses the code in import analysis plugin and tries to find any lines that match accept calls. If it find them, then it means the module is self accepting or it accepts imported modules (depending on parsing result and accept call used). Yes, this means that you can not dynamically accept modules, only static hardcore!
Now lets see the implementation.
vite/packages/vite/src/node/plugins/importAnalysis.ts #importAnalysisPlugin
export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
...
return {
name: 'vite:import-analysis',
async transform(source, importer) {
...
let imports!: readonly ImportSpecifier[]
let exports!: readonly ExportSpecifier[]
source = stripBomTag(source)
try {
;[imports, exports] = parseImports(source)
} catch (_e: unknown) {
const e = _e as EsModuleLexerParseError
const { message, showCodeFrame } = createParseErrorInfo(
importer,
source,
)
this.error(message, showCodeFrame ? e.idx : undefined)
}
Using parseImports function from es module lexer library Vite collects all imports in the source code. It means any import that you do in the code falls into imports array and same with exports.
vite/packages/vite/src/node/plugins/importAnalysis.ts #importAnalysisPlugin
...
await Promise.all(
imports.map(async (importSpecifier, index) => {
const {
s: start,
e: end,
ss: expStart,
se: expEnd,
d: dynamicIndex,
a: attributeIndex,
} = importSpecifier
// check import.meta usage
if (rawUrl === 'import.meta') {
const prop = source.slice(end, end + 4)
if (prop === '.hot') {
hasHMR = true
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0)
if (source.slice(endHot, endHot + 7) === '.accept') {
// further analyze accepted modules
if (source.slice(endHot, endHot + 14) ===
'.acceptExports') {
...
} else {
const importAcceptedUrls =
(orderedAcceptedUrls[index] =
new Set<UrlPosition>())
if (
lexAcceptedHmrDeps(
source,
source.indexOf('(', endHot + 7) + 1,
importAcceptedUrls,
)
) {
isSelfAccepting = true
}
}
}
} else if (prop === '.env') {
hasEnv = true
}
...
}
...
})
Iterating over every import function checks if the import starts with 'import.meta.hot.accept'. If it does, first it is checked if this is acceptExport call (see partial accept in When Files Change section). If it is not, then it must be accepting call and all that is left is to extract modules that this module accepts. If this is self accepting module, toggle the isSelfAccepting flag. The call to lexAcceptedHmrDeps does exactly that, it parses accept expression, and if it in self acceptingform, it returns true, otherwise it puts all accepted modules in importAcceptedUrls set.
Recommended by LinkedIn
Now it is time to inject hot context into the script.
vite/packages/vite/src/node/plugins/importAnalysis.ts #importAnalysisPlugin
if (hasHMR && !ssr && !isClassicWorker) {
...
// inject hot context
str().prepend(
`import { createHotContext as __vite__createHotContext } from
"${clientPublicPath}";` +
`import.meta.hot =
__vite__createHotContext(${JSON.stringify(
normalizeHmrUrl(importerModule.url),
)});`,
)
}
And without further ado this plugin just injects the code that provides HMR API into the script if some conditions are satisfied, like HMR is requested (and calling import.meta.hot in code toggles hasHRM flag).
An now the last part, update module graph with all the collected importation.
vite/packages/vite/src/node/plugins/importAnalysis.ts #importAnalysisPlugin
...
if (!isCSSRequest(importer) || SPECIAL_QUERY_RE.test(importer)) {
...
if (
!isSelfAccepting &&
isPartiallySelfAccepting &&
acceptedExports.size >= exports.length &&
exports.every((e) => acceptedExports.has(e.n))
) {
isSelfAccepting = true
}
const prunedImports = await moduleGraph.updateModuleInfo(
importerModule,
importedUrls,
importedBindings,
normalizedAcceptedUrls,
isPartiallySelfAccepting ? acceptedExports : null,
isSelfAccepting,
staticImportedUrls,
)
...
}
This is actually it... finally... for real. Btw, it is interesting to notice that the order of plugins is important, importAnalysisPlugin just can't work if the plugin that injects accept (like Vite vue plugin) calls is placed after it, importAnalysisPlugin won't see any accept calls in this case.
This was quite a journey, we have investigated how Vite transforms files, how HMR works and the code that enables all this features. I hope you found new information that will help you with your projects or even incentives you to write your own plugins or contribute to Vite. Tell next time!