diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 6cae1256438c42..c198dd23294fdc 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -297,7 +297,14 @@ export interface ViteDevServer { /** * @internal */ - _pendingRequests: Map> + _pendingRequests: Map< + string, + { + request: Promise + timestamp: number + abort: () => void + } + > } export async function createServer( diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 100c9a80b6691d..c8b38c0b31ec77 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -44,22 +44,80 @@ export function transformRequest( options: TransformOptions = {} ): Promise { const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : '') + url - let request = server._pendingRequests.get(cacheKey) - if (!request) { - request = doTransform(url, server, options) - server._pendingRequests.set(cacheKey, request) - const done = () => server._pendingRequests.delete(cacheKey) - request.then(done, done) + + // This module may get invalidated while we are processing it. For example + // when a full page reload is needed after the re-processing of pre-bundled + // dependencies when a missing dep is discovered. We save the current time + // to compare it to the last invalidation performed to know if we should + // cache the result of the transformation or we should discard it as stale. + // + // A module can be invalidated due to: + // 1. A full reload because of pre-bundling newly discovered deps + // 2. A full reload after a config change + // 3. The file that generated the module changed + // 4. Invalidation for a virtual module + // + // For 1 and 2, a new request for this module will be issued after + // the invalidation as part of the browser reloading the page. For 3 and 4 + // there may not be a new request right away because of HMR handling. + // In all cases, the next time this module is requested, it should be + // re-processed. + // + // We save the timestap when we start processing and compare it with the + // last time this module is invalidated + const timestamp = Date.now() + + const pending = server._pendingRequests.get(cacheKey) + if (pending) { + return server.moduleGraph + .getModuleByUrl(removeTimestampQuery(url), options.ssr) + .then((module) => { + if (!module || pending.timestamp > module.lastInvalidationTimestamp) { + // The pending request is still valid, we can safely reuse its result + return pending.request + } else { + // Request 1 for module A (pending.timestamp) + // Invalidate module A (module.lastInvalidationTimestamp) + // Request 2 for module A (timestamp) + + // First request has been invalidated, abort it to clear the cache, + // then perform a new doTransform. + pending.abort() + return transformRequest(url, server, options) + } + }) + } + + const request = doTransform(url, server, options, timestamp) + + // Avoid clearing the cache of future requests if aborted + let cleared = false + const clearCache = () => { + if (!cleared) { + server._pendingRequests.delete(cacheKey) + cleared = true + } } + + // Cache the request and clear it once processing is done + server._pendingRequests.set(cacheKey, { + request, + timestamp, + abort: clearCache + }) + request.then(clearCache, clearCache) + return request } async function doTransform( url: string, server: ViteDevServer, - options: TransformOptions + options: TransformOptions, + timestamp: number ) { url = removeTimestampQuery(url) + const { config, pluginContainer, moduleGraph, watcher } = server const { root, logger } = config const prettyUrl = isDebug ? prettifyUrl(url, root) : '' @@ -81,25 +139,6 @@ async function doTransform( return cached } - // This module may get invalidated while we are processing it. For example - // when a full page reload is needed after the re-processing of pre-bundled - // dependencies when a missing dep is discovered. We save the current time - // to compare it to the last invalidation performed to know if we should - // cache the result of the transformation or we should discard it as stale. - // - // A module can be invalidated due to: - // 1. A full reload because of pre-bundling newly discovered deps - // 2. A full reload after a config change - // 3. The file that generated the module changed - // 4. Invalidation for a virtual module - // - // For 1 and 2, a new request for this module will be issued after - // the invalidation as part of the browser reloading the page. For 3 and 4 - // there may not be a new request right away because of HMR handling. - // In all cases, the next time this module is requested, it should be - // re-processed. - const timestamp = Date.now() - // resolve const id = (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url