Meaning of Code. HMR in Vite. Part 6

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.

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!


To view or add a comment, sign in

More articles by Andrew Laminsky

  • 🧠 LangGraph: How to Finally Make LLMs Think Like a Product?

    Let’s be honest — most of us, as product-minded CTOs, have been in this situation: You build an AI feature, it works…

  • Meaning Of Code. How to Require ('Node.js'). Part 2

    One of the most important parts of Node.js is the collection of modules that fulfill wide variety of tasks, such as…

    1 Comment
  • Meaning Of Code. How to Require ('Node.js'). Part 1.

    I've always loved looking at the endless starry night sky. Its vastness makes you feel like a speck in deep, silent…

    1 Comment
  • Meaning of Code. HMR in Vite. Part 5

    When Files Change First it makes sense to pause a little and make a small detour. What would we expect from Vite to do,…

  • Meaning of Code. HMR in Vite. Part 4

    Just like with html we start with a middleware that is a starting point for transformation - transformMiddleware that…

  • Meaning of Code. HMR in Vite. Part 3

    Ok, so browser is started, we type in the address that Vite http server is running on (say http://localhost:3008). The…

    1 Comment
  • Meaning of Code. HMR in Vite. Part 2

    In the previous article we established a mental modal of how HMR works, now it's time to discover _createServer…

  • Meaning of Code. HMR in Vite. Part 1

    Just like any other software engineer, I get hungry sometimes. So there I was, standing in line at the bakery, eyeing…

Insights from the community

Others also viewed

Explore topics