diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 93de3f19feca20..1c507d1ff000fb 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -55,7 +55,10 @@ import { createWebSocketServer } from './ws' import { baseMiddleware } from './middlewares/base' import { proxyMiddleware } from './middlewares/proxy' import { htmlFallbackMiddleware } from './middlewares/htmlFallback' -import { transformMiddleware } from './middlewares/transform' +import { + cachedTransformMiddleware, + transformMiddleware, +} from './middlewares/transform' import { createDevHtmlTransformFn, indexHtmlMiddleware, @@ -681,7 +684,17 @@ export async function _createServer( if (publicDir && publicFiles) { if (file.startsWith(publicDir)) { - publicFiles[isUnlink ? 'delete' : 'add'](file.slice(publicDir.length)) + const path = file.slice(publicDir.length) + publicFiles[isUnlink ? 'delete' : 'add'](path) + if (!isUnlink) { + const moduleWithSamePath = await moduleGraph.getModuleByUrl(path) + const etag = moduleWithSamePath?.transformResult?.etag + if (etag) { + // The public file should win on the next request over a module with the + // same path. Prevent the transform etag fast path from serving the module + moduleGraph.etagToModuleMap.delete(etag) + } + } } } await handleFileAddUnlink(file, server, isUnlink) @@ -751,6 +764,8 @@ export async function _createServer( middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } + middlewares.use(cachedTransformMiddleware(server)) + // proxy const { proxy } = serverConfig if (proxy) { diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 753a025f98b491..d335e598a6f358 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -44,6 +44,37 @@ const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +/** + * A middleware that short-circuits the middleware chain to serve cached transformed modules + */ +export function cachedTransformMiddleware( + server: ViteDevServer, +): Connect.NextHandleFunction { + // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` + return function viteCachedTransformMiddleware(req, res, next) { + // check if we can return 304 early + const ifNoneMatch = req.headers['if-none-match'] + if (ifNoneMatch) { + const moduleByEtag = server.moduleGraph.getModuleByEtag(ifNoneMatch) + if (moduleByEtag?.transformResult?.etag === ifNoneMatch) { + // For direct CSS requests, if the same CSS file is imported in a module, + // the browser sends the request for the direct CSS request with the etag + // from the imported CSS module. We ignore the etag in this case. + const mixedEtag = + !req.headers.accept?.includes('text/css') && + isDirectRequest(moduleByEtag.url) + if (!mixedEtag) { + debugCache?.(`[304] ${prettifyUrl(req.url!, server.config.root)}`) + res.statusCode = 304 + return res.end() + } + } + } + + next() + } +} + export function transformMiddleware( server: ViteDevServer, ): Connect.NextHandleFunction { @@ -155,18 +186,6 @@ export function transformMiddleware( url = injectQuery(url, 'direct') } - // check if we can return 304 early - const ifNoneMatch = req.headers['if-none-match'] - if ( - ifNoneMatch && - (await server.moduleGraph.getModuleByUrl(url, false))?.transformResult - ?.etag === ifNoneMatch - ) { - debugCache?.(`[304] ${prettifyUrl(url, server.config.root)}`) - res.statusCode = 304 - return res.end() - } - // resolve, load and transform using the plugin container const result = await transformRequest(url, server, { html: req.headers.accept?.includes('text/html'), diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 435b3876dde3ab..40efa65f32b67e 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -88,6 +88,7 @@ export type ResolvedUrl = [ export class ModuleGraph { urlToModuleMap = new Map() idToModuleMap = new Map() + etagToModuleMap = new Map() // a single file may corresponds to multiple modules with different queries fileToModulesMap = new Map>() safeModulesPath = new Set() @@ -192,6 +193,9 @@ export class ModuleGraph { // Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline // Invalidating the transform result is enough to ensure this module is re-processed next time it is requested + const etag = mod.transformResult?.etag + if (etag) this.etagToModuleMap.delete(etag) + mod.transformResult = null mod.ssrTransformResult = null mod.ssrModule = null @@ -419,6 +423,27 @@ export class ModuleGraph { return this._resolveUrl(url, ssr) } + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ssr: boolean, + ): void { + if (ssr) { + mod.ssrTransformResult = result + } else { + const prevEtag = mod.transformResult?.etag + if (prevEtag) this.etagToModuleMap.delete(prevEtag) + + mod.transformResult = result + + if (result?.etag) this.etagToModuleMap.set(result.etag, mod) + } + } + + getModuleByEtag(etag: string): ModuleNode | undefined { + return this.etagToModuleMap.get(etag) + } + /** * @internal */ diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index e3ce47fc1a86fa..a6fce5d6634d39 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -361,10 +361,8 @@ async function loadAndTransform( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale - if (timestamp > mod.lastInvalidationTimestamp) { - if (ssr) mod.ssrTransformResult = result - else mod.transformResult = result - } + if (timestamp > mod.lastInvalidationTimestamp) + moduleGraph.updateModuleTransformResult(mod, result, ssr) return result } @@ -465,10 +463,8 @@ async function handleModuleSoftInvalidation( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale - if (timestamp > mod.lastInvalidationTimestamp) { - if (ssr) mod.ssrTransformResult = result - else mod.transformResult = result - } + if (timestamp > mod.lastInvalidationTimestamp) + server.moduleGraph.updateModuleTransformResult(mod, result, ssr) return result }