From 56ded34020dee43e6947be879f34006f88eef5db Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 23 Oct 2026 19:46:22 +0200 Subject: [PATCH] fix(core): evict the bundler persistent cache when the target browsers, the server Node.js target or plugin versions change (AI-assisted) The persistent cache doesn't track loader options, so getCacheVersion() must include what they're computed from: - The targets from the new getTargets(): the browsers the client queries match, instead of the queries. The same queries (like "baseline widely available") can match other browsers over time or after a Browserslist data upgrade. For the server cache, the server targets (current Node.js version or DOCUSAURUS_SERVER_NODE_TARGET). - The plugin versions: upgrading a third-party plugin can change the loader options it computes without changing any build dependency. Co-Authored-By: Claude Opus 5.5 --- .../docusaurus-bundler/src/browserslist.ts | 14 +++ packages/docusaurus-bundler/src/index.ts | 2 +- .../src/webpack/__tests__/base.test.ts | 94 ++++++++++++++++--- packages/docusaurus/src/webpack/base.ts | 29 ++++-- 4 files changed, 117 insertions(+), 22 deletions(-) diff --git a/packages/docusaurus-bundler/src/browserslist.ts b/packages/docusaurus-bundler/src/browserslist.ts index 2acf4a8a14c8..725cca50a033 100644 --- a/packages/docusaurus-bundler/src/browserslist.ts +++ b/packages/docusaurus-bundler/src/browserslist.ts @@ -14,6 +14,20 @@ import type {CurrentBundler} from '@docusaurus/types'; // See https://web.dev/baseline const DefaultBrowserslistQueries = ['baseline widely available']; +/** + * The targets the client or server bundle is compiled for, like + * `['chrome 120', 'firefox 115']` or `['node 24.11.1']` + */ +export function getTargets({ + isServer, + bundlerName, +}: { + isServer: boolean; + bundlerName: CurrentBundler['name']; +}): string[] { + return isServer ? getServerTargets({bundlerName}) : getClientTargets(); +} + export function getClientQueries(): string[] { return ( browserslist.loadConfig({path: process.cwd()}) ?? DefaultBrowserslistQueries diff --git a/packages/docusaurus-bundler/src/index.ts b/packages/docusaurus-bundler/src/index.ts index 4f34e0f646b7..803ecb3dc176 100644 --- a/packages/docusaurus-bundler/src/index.ts +++ b/packages/docusaurus-bundler/src/index.ts @@ -15,7 +15,7 @@ export { registerBundlerTracing, } from './currentBundler'; -export {getClientQueries} from './browserslist'; +export {getClientQueries, getTargets} from './browserslist'; export {getMinimizers} from './minification'; export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml'; export {createJsLoaderFactory} from './loaders/jsLoader'; diff --git a/packages/docusaurus/src/webpack/__tests__/base.test.ts b/packages/docusaurus/src/webpack/__tests__/base.test.ts index 7c7ae4004ee4..887e0902e4c1 100644 --- a/packages/docusaurus/src/webpack/__tests__/base.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/base.test.ts @@ -13,7 +13,7 @@ import {getCurrentBundler} from '@docusaurus/bundler'; import {excludeJS, clientDir, createBaseConfig} from '../base'; import {BundlerNames, createTestConfigureWebpackUtils} from './testUtils'; import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation'; -import type {CurrentBundler, Props} from '@docusaurus/types'; +import type {CurrentBundler, Props, SiteMetadata} from '@docusaurus/types'; describe('babel transpilation exclude logic', () => { it('always transpiles client dir files', () => { @@ -77,6 +77,7 @@ describe('base webpack config', () => { }, siteMetadata: { docusaurusVersion: '2.0.0-alpha.70', + pluginVersions: {}, }, plugins: [ { @@ -160,24 +161,95 @@ describe('base webpack config', () => { vi.unstubAllEnvs(); }); - async function getCacheVersion(browserslistQuery: string) { - // Browserslist reads this env variable before any config file - vi.stubEnv('BROWSERSLIST', browserslistQuery); + async function getCacheVersion({ + isServer = false, + siteMetadata = props.siteMetadata, + }: { + isServer?: boolean; + siteMetadata?: SiteMetadata; + } = {}) { const config = await createBaseConfig({ - props: await createProps(), - isServer: false, + props: {...(await createProps()), siteMetadata}, + isServer, minify: true, configureWebpackUtils: await createTestConfigureWebpackUtils(), }); return (config.cache as {version: string}).version; } - it('is evicted when the Browserslist queries change', async () => { - const oldBrowsersVersion = await getCacheVersion('chrome 90'); - const baselineVersion = await getCacheVersion( - 'baseline widely available', - ); + // Browserslist reads this env variable before any config file + function stubBrowserslistQuery(query: string) { + vi.stubEnv('BROWSERSLIST', query); + } + + function stubServerNodeTarget(nodeVersion: string) { + vi.stubEnv('DOCUSAURUS_SERVER_NODE_TARGET', nodeVersion); + } + + function createSiteMetadata({ + siteVersion, + pluginVersion = '1.0.0', + }: { + siteVersion?: string; + pluginVersion?: string; + }): SiteMetadata { + return { + ...props.siteMetadata, + siteVersion, + pluginVersions: { + 'third-party-plugin': { + type: 'package', + name: 'third-party-plugin', + version: pluginVersion, + }, + }, + }; + } + + it('is evicted when the Browserslist queries match other browsers', async () => { + stubBrowserslistQuery('chrome 90'); + const oldBrowsersVersion = await getCacheVersion(); + stubBrowserslistQuery('baseline widely available'); + const baselineVersion = await getCacheVersion(); expect(oldBrowsersVersion).not.toBe(baselineVersion); }); + + it('is not evicted when other Browserslist queries match the same browsers', async () => { + stubBrowserslistQuery('chrome 90'); + const versionQueryVersion = await getCacheVersion(); + stubBrowserslistQuery('chrome >= 90 and chrome <= 90'); + const rangeQueryVersion = await getCacheVersion(); + expect(versionQueryVersion).toBe(rangeQueryVersion); + }); + + // Server targets aren't Browserslist queries: Browserslist doesn't know + // these Node.js patch releases + it('is evicted when the server Node.js target changes', async () => { + stubServerNodeTarget('24.11.1'); + const oldNodeVersion = await getCacheVersion({isServer: true}); + stubServerNodeTarget('24.11.2'); + const newNodeVersion = await getCacheVersion({isServer: true}); + expect(oldNodeVersion).not.toBe(newNodeVersion); + }); + + it('is evicted when a plugin package version changes', async () => { + const oldPluginVersion = await getCacheVersion({ + siteMetadata: createSiteMetadata({pluginVersion: '1.0.0'}), + }); + const newPluginVersion = await getCacheVersion({ + siteMetadata: createSiteMetadata({pluginVersion: '1.1.0'}), + }); + expect(oldPluginVersion).not.toBe(newPluginVersion); + }); + + it('is not evicted when the site version changes', async () => { + const oldSiteVersion = await getCacheVersion({ + siteMetadata: createSiteMetadata({siteVersion: '1.0.0'}), + }); + const newSiteVersion = await getCacheVersion({ + siteMetadata: createSiteMetadata({siteVersion: '2.0.0'}), + }); + expect(oldSiteVersion).toBe(newSiteVersion); + }); }); }); diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts index 4f31f6801af7..2d73953fb460 100644 --- a/packages/docusaurus/src/webpack/base.ts +++ b/packages/docusaurus/src/webpack/base.ts @@ -8,9 +8,9 @@ import path from 'node:path'; import { createJsLoaderFactory, - getClientQueries, getCSSExtractPlugin, getMinimizers, + getTargets, } from '@docusaurus/bundler'; import { ASSET_INLINE_MAX_SIZE, @@ -109,15 +109,24 @@ export async function createBaseConfig({ // When the version string changes, the cache is evicted function getCacheVersion() { - // Because Webpack does not evict the cache on alias/swizzle changes, - // See https://github.com/webpack/webpack/issues/13627 - const themeAliasesHash = md5Hash(JSON.stringify(themeAliases)); - // Loader and minimizer options are not part of the cache key - // Changing the Browserslist config must evict the cache - const browserslistHash = md5Hash(JSON.stringify(getClientQueries())); - // Asset module parser options are not part of the cache key - // Changing the asset inline max size must evict the cache - return `${siteMetadata.docusaurusVersion}-${themeAliasesHash}-${browserslistHash}-${ASSET_INLINE_MAX_SIZE}`; + const versionParts: unknown[] = [ + siteMetadata.docusaurusVersion, + // Because Webpack does not evict the cache on alias/swizzle changes, + // See https://github.com/webpack/webpack/issues/13627 + themeAliases, + // Loader and minimizer options are not part of the cache key + // Changing the target browsers or Node.js version must evict the cache + // The same Browserslist queries can match other browsers over time and + // after data upgrades, for example "baseline widely available" + getTargets({isServer, bundlerName: props.currentBundler.name}), + // The plugin code computing loader options is not a build dependency + // Upgrading a third-party plugin must evict the cache + siteMetadata.pluginVersions, + // Asset module parser options are not part of the cache key + // Changing the asset inline max size must evict the cache + ASSET_INLINE_MAX_SIZE, + ]; + return md5Hash(JSON.stringify(versionParts)); } // When one of those modules/dependencies change (including transitive