From f5c43b25b485cc1acc952d3d4556994fe3ccbe1c Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 12:27:21 +0100 Subject: [PATCH 1/6] feat(core): promote experimental_storage to stable top-level storage config The `future.experimental_storage` config has been promoted to a stable top-level `storage` site config attribute. Using the old config path now throws a clear error pointing users to the new location. Also adds a `future.v4.siteStorageNamespacing` flag (default: false) that changes the default of `storage.namespace` to `true`, enabling automatic browser storage key namespacing in v4. Co-Authored-By: Claude Opus 4.6 --- packages/docusaurus-types/src/config.d.ts | 9 +- .../__snapshots__/config.test.ts.snap | 90 ++++++----- .../__tests__/__snapshots__/site.test.ts.snap | 108 +++++++------ .../server/__tests__/configValidation.test.ts | 150 +++++++++++------- .../src/server/__tests__/storage.test.ts | 18 +-- .../docusaurus/src/server/configValidation.ts | 34 +++- packages/docusaurus/src/server/storage.ts | 14 +- website/docs/api/docusaurus.config.js.mdx | 26 ++- website/docusaurus.config.ts | 3 - 9 files changed, 267 insertions(+), 185 deletions(-) diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index bc4180ca7b37..0ce567b4a20e 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -39,6 +39,7 @@ export type FasterConfig = { export type FutureV4Config = { removeLegacyPostBuildHeadAttribute: boolean; useCssCascadeLayers: boolean; + siteStorageNamespacing: boolean; }; // VCS (Version Control System) info about a given change, e.g., a git commit. @@ -96,8 +97,6 @@ export type FutureConfig = { experimental_faster: FasterConfig; - experimental_storage: StorageConfig; - experimental_vcs: VcsConfig; /** @@ -175,6 +174,12 @@ export type DocusaurusConfig = { * @see https://docusaurus.io/docs/api/docusaurus-config#i18n */ i18n: I18nConfig; + /** + * Site-wide browser storage options. + * + * @see https://docusaurus.io/docs/api/docusaurus-config#storage + */ + storage: StorageConfig; /** * Docusaurus future flags and experimental features. * Similar to Remix future flags, see https://remix.run/blog/future-flags diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 091d94e9ac18..704bb6aee625 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -20,10 +20,6 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -31,6 +27,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -73,6 +70,10 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -105,10 +106,6 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -116,6 +113,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -158,6 +156,10 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -190,10 +192,6 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -201,6 +199,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -243,6 +242,10 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -275,10 +278,6 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -286,6 +285,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -328,6 +328,10 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -360,10 +364,6 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -371,6 +371,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -413,6 +414,10 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -445,10 +450,6 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -456,6 +457,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -498,6 +500,10 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -530,10 +536,6 @@ exports[`loadSiteConfig website with valid async config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -541,6 +543,7 @@ exports[`loadSiteConfig website with valid async config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -585,6 +588,10 @@ exports[`loadSiteConfig website with valid async config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "Hello World", "themeConfig": {}, @@ -617,10 +624,6 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -628,6 +631,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -672,6 +676,10 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "Hello World", "themeConfig": {}, @@ -704,10 +712,6 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -715,6 +719,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -759,6 +764,10 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "Hello World", "themeConfig": {}, @@ -794,10 +803,6 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -805,6 +810,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -857,6 +863,10 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "Hello World", "themeConfig": {}, diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap index 6990d4d4a006..08fc5ae5ca38 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap @@ -100,10 +100,6 @@ exports[`loadSite custom-i18n-site loads site 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -111,6 +107,7 @@ exports[`loadSite custom-i18n-site loads site 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -161,6 +158,10 @@ exports[`loadSite custom-i18n-site loads site 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -273,10 +274,6 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom config 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -284,6 +281,7 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom config 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -326,6 +324,10 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom config 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -438,10 +440,6 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom outDir 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -449,6 +447,7 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom outDir 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -491,6 +490,10 @@ exports[`loadSite simple-site-with-baseUrl loads site - custom outDir 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -603,10 +606,6 @@ exports[`loadSite simple-site-with-baseUrl loads site 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -614,6 +613,7 @@ exports[`loadSite simple-site-with-baseUrl loads site 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -656,6 +656,10 @@ exports[`loadSite simple-site-with-baseUrl loads site 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -812,10 +816,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr + custom "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -823,6 +823,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr + custom }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -887,6 +888,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr + custom "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -1043,10 +1048,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - custom outDir 1`] = "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -1054,6 +1055,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - custom outDir 1`] = }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -1118,6 +1120,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - custom outDir 1`] = "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -1274,10 +1280,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale de 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -1285,6 +1287,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale de 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -1349,6 +1352,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale de 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -1505,10 +1512,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale en 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -1516,6 +1519,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale en 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -1580,6 +1584,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale en 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -1736,10 +1744,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale es 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -1747,6 +1751,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale es 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -1811,6 +1816,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale es 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -1967,10 +1976,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -1978,6 +1983,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -2042,6 +2048,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale fr 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -2198,10 +2208,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale it 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -2209,6 +2215,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale it 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -2273,6 +2280,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site - locale it 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, @@ -2429,10 +2440,6 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site 1`] = ` "swcJsMinimizer": false, }, "experimental_router": "browser", - "experimental_storage": { - "namespace": false, - "type": "localStorage", - }, "experimental_vcs": { "getFileCreationInfo": [Function], "getFileLastUpdateInfo": [Function], @@ -2440,6 +2447,7 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site 1`] = ` }, "v4": { "removeLegacyPostBuildHeadAttribute": false, + "siteStorageNamespacing": false, "useCssCascadeLayers": false, }, }, @@ -2504,6 +2512,10 @@ exports[`loadSite simple-site-with-baseUrl-i18n loads site 1`] = ` "staticDirectories": [ "static", ], + "storage": { + "namespace": false, + "type": "localStorage", + }, "stylesheets": [], "tagline": "", "themeConfig": {}, diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 379ce8af2898..a1b65394f162 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -57,10 +57,15 @@ describe('normalizeConfig', () => { const userConfig: Config = { ...DEFAULT_CONFIG, ...baseConfig, + storage: { + type: 'sessionStorage', + namespace: true, + }, future: { v4: { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }, experimental_faster: { swcJsLoader: true, @@ -73,10 +78,6 @@ describe('normalizeConfig', () => { ssgWorkerThreads: true, gitEagerVcs: true, }, - experimental_storage: { - type: 'sessionStorage', - namespace: true, - }, experimental_vcs: { initialize: (_params) => {}, getFileCreationInfo: (_filePath) => null, @@ -1088,6 +1089,7 @@ describe('future', () => { v4: { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }, experimental_faster: { swcJsLoader: true, @@ -1105,10 +1107,6 @@ describe('future', () => { getFileCreationInfo: (_filePath) => null, getFileLastUpdateInfo: (_filePath) => null, }, - experimental_storage: { - type: 'sessionStorage', - namespace: 'myNamespace', - }, experimental_router: 'hash', }; expect( @@ -1217,27 +1215,33 @@ describe('future', () => { describe('storage', () => { function storageContaining(storage: Partial) { - return futureContaining({ - experimental_storage: expect.objectContaining(storage), + return expect.objectContaining({ + storage: expect.objectContaining(storage), }); } it('accepts storage - undefined', () => { expect( normalizeConfig({ - future: { - experimental_storage: undefined, - }, + storage: undefined, }), - ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG)); + ).toEqual( + expect.objectContaining({ + storage: DEFAULT_STORAGE_CONFIG, + }), + ); }); it('accepts storage - empty', () => { expect( normalizeConfig({ - future: {experimental_storage: {}}, + storage: {}, }), - ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG)); + ).toEqual( + expect.objectContaining({ + storage: DEFAULT_STORAGE_CONFIG, + }), + ); }); it('accepts storage - full', () => { @@ -1247,9 +1251,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); @@ -1259,12 +1261,10 @@ describe('future', () => { const storage: Partial = true; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage" must be of type object + ""storage" must be of type object " `); }); @@ -1272,14 +1272,29 @@ describe('future', () => { it('rejects storage - number', () => { // @ts-expect-error: invalid const storage: Partial = 42; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage" must be of type object + " + `); + }); + + it('rejects future.experimental_storage', () => { expect(() => normalizeConfig({ future: { - experimental_storage: storage, + // @ts-expect-error: testing removed config + experimental_storage: { + type: 'sessionStorage', + namespace: true, + }, }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage" must be of type object + ""future.experimental_storage" failed custom validation because The Docusaurus config \`future.experimental_storage\` has been promoted to a stable top-level \`storage\` config attribute. Please move your storage config to the top level. " `); }); @@ -1291,9 +1306,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual( storageContaining({ @@ -1309,9 +1322,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining({type: 'localStorage'})); }); @@ -1321,13 +1332,11 @@ describe('future', () => { const storage: Partial = {type: 42}; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] - "future.experimental_storage.type" must be a string + ""storage.type" must be one of [localStorage, sessionStorage] + "storage.type" must be a string " `); }); @@ -1337,13 +1346,11 @@ describe('future', () => { const storage: Partial = {type: 42}; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] - "future.experimental_storage.type" must be a string + ""storage.type" must be one of [localStorage, sessionStorage] + "storage.type" must be a string " `); }); @@ -1353,12 +1360,10 @@ describe('future', () => { const storage: Partial = {type: 'badType'}; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] + ""storage.type" must be one of [localStorage, sessionStorage] " `); }); @@ -1371,9 +1376,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); @@ -1384,23 +1387,54 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); + it('defaults namespace to false', () => { + expect( + normalizeConfig({ + storage: {}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('defaults namespace to true when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('keeps explicit namespace false even when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: false}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace string when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: 'custom'}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: 'custom'})); + }); + it('rejects namespace - null', () => { const storage: Partial = {namespace: null}; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage.namespace" must be one of [string, boolean] + ""storage.namespace" must be one of [string, boolean] " `); }); @@ -1410,12 +1444,10 @@ describe('future', () => { const storage: Partial = {namespace: 42}; expect(() => normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage.namespace" must be one of [string, boolean] + ""storage.namespace" must be one of [string, boolean] " `); }); diff --git a/packages/docusaurus/src/server/__tests__/storage.test.ts b/packages/docusaurus/src/server/__tests__/storage.test.ts index f91b9ddbef3e..57999a6dc454 100644 --- a/packages/docusaurus/src/server/__tests__/storage.test.ts +++ b/packages/docusaurus/src/server/__tests__/storage.test.ts @@ -6,11 +6,8 @@ */ import {createSiteStorage} from '../storage'; -import { - DEFAULT_FUTURE_CONFIG, - DEFAULT_STORAGE_CONFIG, -} from '../configValidation'; -import type {FutureConfig, StorageConfig, SiteStorage} from '@docusaurus/types'; +import {DEFAULT_STORAGE_CONFIG} from '../configValidation'; +import type {StorageConfig, SiteStorage} from '@docusaurus/types'; function test({ url = 'https://docusaurus.io', @@ -21,15 +18,14 @@ function test({ baseUrl?: string; storage?: Partial; }): SiteStorage { - const future: FutureConfig = { - ...DEFAULT_FUTURE_CONFIG, - experimental_storage: { + return createSiteStorage({ + url, + baseUrl, + storage: { ...DEFAULT_STORAGE_CONFIG, ...storage, }, - }; - - return createSiteStorage({url, baseUrl, future}); + }); } const DefaultSiteStorage: SiteStorage = { diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index e57a7ba45abe..6ded56a36536 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -100,18 +100,19 @@ export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = { export const DEFAULT_FUTURE_V4_CONFIG: FutureV4Config = { removeLegacyPostBuildHeadAttribute: false, useCssCascadeLayers: false, + siteStorageNamespacing: false, }; // When using the "v4: true" shortcut export const DEFAULT_FUTURE_V4_CONFIG_TRUE: FutureV4Config = { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }; export const DEFAULT_FUTURE_CONFIG: FutureConfig = { v4: DEFAULT_FUTURE_V4_CONFIG, experimental_faster: DEFAULT_FASTER_CONFIG, - experimental_storage: DEFAULT_STORAGE_CONFIG, experimental_vcs: getVcsPreset('default-v1'), experimental_router: 'browser', }; @@ -142,6 +143,7 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = { export const DEFAULT_CONFIG: Pick< DocusaurusConfig, | 'i18n' + | 'storage' | 'future' | 'onBrokenLinks' | 'onBrokenAnchors' @@ -164,6 +166,7 @@ export const DEFAULT_CONFIG: Pick< | 'markdown' > = { i18n: DEFAULT_I18N_CONFIG, + storage: DEFAULT_STORAGE_CONFIG, future: DEFAULT_FUTURE_CONFIG, onBrokenLinks: 'throw', onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw @@ -318,6 +321,9 @@ const FUTURE_V4_SCHEMA = Joi.alternatives() useCssCascadeLayers: Joi.boolean().default( DEFAULT_FUTURE_V4_CONFIG.useCssCascadeLayers, ), + siteStorageNamespacing: Joi.boolean().default( + DEFAULT_FUTURE_V4_CONFIG.siteStorageNamespacing, + ), }), Joi.boolean() .required() @@ -332,12 +338,13 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({ type: Joi.string() .equal('localStorage', 'sessionStorage') .default(DEFAULT_STORAGE_CONFIG.type), - namespace: Joi.alternatives() - .try(Joi.string(), Joi.boolean()) - .default(DEFAULT_STORAGE_CONFIG.namespace), + // namespace default is not set here on purpose + // It is resolved in postProcessDocusaurusConfig based on + // the future.v4.siteStorageNamespacing flag + namespace: Joi.alternatives().try(Joi.string(), Joi.boolean()), }) .optional() - .default(DEFAULT_STORAGE_CONFIG); + .default({type: DEFAULT_STORAGE_CONFIG.type}); const VCS_CONFIG_OBJECT_SCHEMA = Joi.object({ // All the fields are required on purpose @@ -372,11 +379,19 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, - experimental_storage: STORAGE_CONFIG_SCHEMA, experimental_vcs: VCS_CONFIG_SCHEMA, experimental_router: Joi.string() .equal('browser', 'hash') .default(DEFAULT_FUTURE_CONFIG.experimental_router), + experimental_storage: Joi.any().custom(() => { + throw new Error( + `The Docusaurus config ${logger.code( + 'future.experimental_storage', + )} has been promoted to a stable top-level ${logger.code( + 'storage', + )} config attribute. Please move your storage config to the top level.`, + ); + }), }) .optional() .default(DEFAULT_FUTURE_CONFIG); @@ -390,6 +405,7 @@ export const ConfigSchema = Joi.object({ title: Joi.string().required(), trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior! i18n: I18N_CONFIG_SCHEMA, + storage: STORAGE_CONFIG_SCHEMA, future: FUTURE_CONFIG_SCHEMA, onBrokenLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') @@ -533,6 +549,12 @@ export const ConfigSchema = Joi.object({ // Expressing this kind of logic in Joi is a pain // We also want to decouple logic from Joi: easier to remove it later! function postProcessDocusaurusConfig(config: DocusaurusConfig) { + // Resolve storage.namespace based on the v4 future flag + // undefined means "not explicitly set by user" + if (config.storage.namespace === undefined) { + config.storage.namespace = config.future.v4.siteStorageNamespacing; + } + if (config.onBrokenMarkdownLinks) { logger.warn`The code=${'siteConfig.onBrokenMarkdownLinks'} config option is deprecated and will be removed in Docusaurus v4. Please migrate and move this option to code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} instead.`; diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 657f4a359c2d..932db7df2695 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -9,11 +9,7 @@ import {normalizeUrl, simpleHash} from '@docusaurus/utils'; import {addTrailingSlash} from '@docusaurus/utils-common'; import type {DocusaurusConfig, SiteStorage} from '@docusaurus/types'; -type PartialFuture = Pick; - -type PartialConfig = Pick & { - future: PartialFuture; -}; +type PartialConfig = Pick; function automaticNamespace(config: PartialConfig): string { const normalizedUrl = addTrailingSlash( @@ -23,17 +19,17 @@ function automaticNamespace(config: PartialConfig): string { } function getNamespaceString(config: PartialConfig): string | null { - if (config.future.experimental_storage.namespace === true) { + if (config.storage.namespace === true) { return automaticNamespace(config); - } else if (config.future.experimental_storage.namespace === false) { + } else if (config.storage.namespace === false) { return null; } else { - return config.future.experimental_storage.namespace; + return config.storage.namespace; } } export function createSiteStorage(config: PartialConfig): SiteStorage { - const {type} = config.future.experimental_storage; + const {type} = config.storage; const namespaceString = getNamespaceString(config); const namespace = namespaceString ? `-${namespaceString}` : ''; diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 5acad437ccc7..4fcada098324 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -208,6 +208,24 @@ export default { - `url`: This lets you override the [`siteConfig.url`](#url), particularly useful if your site is [deployed over multiple domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment). - `baseUrl`: This lets you override the default localized `baseUrl` Docusaurus infers from your [`siteConfig.baseUrl`](#baseUrl), giving you more control to host your localized site in less common ways, in particularly [deployments over multi-domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment) +### `storage` {/* #storage */} + +- Type: `Object` + +Site-wide browser storage options that theme authors should strive to respect. + +```js title="docusaurus.config.js" +export default { + storage: { + type: 'localStorage', + namespace: true, + }, +}; +``` + +- `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`. +- `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior). Use the [`future.v4.siteStorageNamespacing`](#future) flag to default this to `true`. + ### `future` {/* #future */} - Type: `Object` @@ -245,10 +263,6 @@ export default { ssgWorkerThreads: true, mdxCrossCompilerCache: true, }, - experimental_storage: { - type: 'localStorage', - namespace: true, - }, experimental_router: 'hash', }, }; @@ -257,6 +271,7 @@ export default { - `v4`: Permits to opt-in for upcoming Docusaurus v4 breaking changes and features, to prepare your site in advance for this new version. Use `true` as a shorthand to enable all the flags. - [`removeLegacyPostBuildHeadAttribute`](https://github.com/facebook/docusaurus/pull/10435): Removes the legacy `plugin.postBuild({head})` API that prevents us from applying useful SSG optimizations ([explanations](https://github.com/facebook/docusaurus/pull/10850)). - [`useCssCascadeLayers`](https://github.com/facebook/docusaurus/pull/11142): This enables the [Docusaurus CSS Cascade Layers plugin](./plugins/plugin-css-cascade-layers.mdx) with pre-configured layers that we plan to apply by default for Docusaurus v4. + - `siteStorageNamespacing`: Defaults the [`storage.namespace`](#storage) config to `true` instead of `false`. This enables automatic browser storage key namespacing, which avoids storage key conflicts when multiple Docusaurus sites are hosted under the same domain, or on localhost. - `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags. Read more on the [Docusaurus Faster](https://github.com/facebook/docusaurus/issues/10556) issue. Available feature flags: - [`swcJsLoader`](https://github.com/facebook/docusaurus/pull/10435): Use [SWC](https://swc.rs/) to transpile JS (instead of [Babel](https://babeljs.io/)). - [`swcJsMinimizer`](https://github.com/facebook/docusaurus/pull/10441): Use [SWC](https://swc.rs/) to minify JS (instead of [Terser](https://github.com/terser/terser)). @@ -267,9 +282,6 @@ export default { - [`mdxCrossCompilerCache`](https://github.com/facebook/docusaurus/pull/10479): Compile MDX files only once for both browser/Node.js environments instead of twice. - [`ssgWorkerThreads`](https://github.com/facebook/docusaurus/pull/10826): Using a Node.js worker thread pool to execute the static site generation phase faster. Requires `future.v4.removeLegacyPostBuildHeadAttribute` to be turned on. - [`gitEagerVcs`](https://github.com/facebook/docusaurus/pull/11512): Upgrades the default [VCS strategy](#vcs) to `default-v2`, that reads your whole Git repository at once instead of per-file, making Git operations faster on large repositories. -- `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect. - - `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`. - - `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior). - `experimental_router`: The router type to use. Possible values are `browser` and `hash`. Defaults to `browser`. The `hash` router is only useful for rare cases where you want to opt-out of static site generation, have a fully client-side app with a single `index.html` entrypoint file. This can be useful to distribute a Docusaurus site as a `.zip` archive that you can [browse locally without running a web server](https://github.com/facebook/docusaurus/issues/3825). - [`experimental_vcs`](#vcs): The Version Control System (VCS) implementation to use to read file info (creation/last update date/author). Read the [dedicated section](#vcs) below for details. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index f242e5aba33d..f49ef0c0247b 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -193,9 +193,6 @@ export default async function createConfigAsync() { ssgWorkerThreads: true, gitEagerVcs: true, }, - experimental_storage: { - namespace: true, - }, experimental_vcs: vcs, experimental_router: router, }, From 41de51be6fe0fd940957171d4284e53786803419 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 12:34:04 +0100 Subject: [PATCH 2/6] fix(core): remove Joi generic type param to fix TS build The Joi.object generic enforced the type, but experimental_storage is no longer on FutureConfig. Remove the generic to allow the extra key for the error-throwing validator. Co-Authored-By: Claude Opus 4.6 --- packages/docusaurus/src/server/configValidation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 6ded56a36536..ffa95529aee0 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -376,7 +376,7 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { return value; }).default(true); -const FUTURE_CONFIG_SCHEMA = Joi.object({ +const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, experimental_vcs: VCS_CONFIG_SCHEMA, From 856cf40e55b99f93b932fd73e8ed2b7fa4ae25c1 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 14:16:29 +0100 Subject: [PATCH 3/6] fix: address PR review comments for stable storage config - Use Joi .forbidden() instead of .custom() for cleaner error messages - Move storageContaining() helper to describe('future') scope for reuse - Add dedicated describe('siteStorageNamespacing') test block - Add test for siteStorageNamespacing = false - Add siteStorageNamespacing to v4 flags example in docs - Use storageContaining() helper consistently in storage tests Co-Authored-By: Claude Opus 4.6 --- .../server/__tests__/configValidation.test.ts | 91 ++++++++++--------- .../docusaurus/src/server/configValidation.ts | 14 +-- website/docs/api/docusaurus.config.js.mdx | 1 + 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index a1b65394f162..61069ebe0ee8 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -1068,6 +1068,12 @@ describe('future', () => { }); } + function storageContaining(storage: Partial) { + return expect.objectContaining({ + storage: expect.objectContaining(storage), + }); + } + it('accepts future - undefined', () => { expect( normalizeConfig({ @@ -1214,22 +1220,12 @@ describe('future', () => { }); describe('storage', () => { - function storageContaining(storage: Partial) { - return expect.objectContaining({ - storage: expect.objectContaining(storage), - }); - } - it('accepts storage - undefined', () => { expect( normalizeConfig({ storage: undefined, }), - ).toEqual( - expect.objectContaining({ - storage: DEFAULT_STORAGE_CONFIG, - }), - ); + ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); }); it('accepts storage - empty', () => { @@ -1237,11 +1233,7 @@ describe('future', () => { normalizeConfig({ storage: {}, }), - ).toEqual( - expect.objectContaining({ - storage: DEFAULT_STORAGE_CONFIG, - }), - ); + ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); }); it('accepts storage - full', () => { @@ -1294,7 +1286,7 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage" failed custom validation because The Docusaurus config \`future.experimental_storage\` has been promoted to a stable top-level \`storage\` config attribute. Please move your storage config to the top level. + "The Docusaurus config \`future.experimental_storage\` has been promoted to a stable top-level \`storage\` config attribute. Please move your storage config to the top level. " `); }); @@ -1400,33 +1392,6 @@ describe('future', () => { ).toEqual(storageContaining({namespace: false})); }); - it('defaults namespace to true when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: true})); - }); - - it('keeps explicit namespace false even when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: false}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('keeps explicit namespace string when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: 'custom'}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: 'custom'})); - }); - it('rejects namespace - null', () => { const storage: Partial = {namespace: null}; expect(() => @@ -1454,6 +1419,44 @@ describe('future', () => { }); }); + describe('siteStorageNamespacing', () => { + it('defaults namespace to true when siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('defaults namespace to false when siteStorageNamespacing is false', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: false}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace false even when siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: false}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace string when siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: 'custom'}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: 'custom'})); + }); + }); + describe('vcs', () => { function vcsContaining(vcs: Partial) { return futureContaining({ diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index ffa95529aee0..9f64189efacc 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -376,22 +376,24 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { return value; }).default(true); -const FUTURE_CONFIG_SCHEMA = Joi.object({ +const FUTURE_CONFIG_SCHEMA = Joi.object< + FutureConfig & {experimental_storage: never} +>({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, experimental_vcs: VCS_CONFIG_SCHEMA, experimental_router: Joi.string() .equal('browser', 'hash') .default(DEFAULT_FUTURE_CONFIG.experimental_router), - experimental_storage: Joi.any().custom(() => { - throw new Error( - `The Docusaurus config ${logger.code( + experimental_storage: Joi.any() + .forbidden() + .messages({ + 'any.unknown': `The Docusaurus config ${logger.code( 'future.experimental_storage', )} has been promoted to a stable top-level ${logger.code( 'storage', )} config attribute. Please move your storage config to the top level.`, - ); - }), + }), }) .optional() .default(DEFAULT_FUTURE_CONFIG); diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 4fcada098324..6dd7bec969b9 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -252,6 +252,7 @@ export default { v4: { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }, experimental_faster: { swcJsLoader: true, From 3f6b593097c15c2a86d5647faa7ef02723a55f98 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 14:19:26 +0100 Subject: [PATCH 4/6] fix: move v4 flag interaction tests to storage, add siteStorageNamespacing unit tests under v4 Storage+v4 namespace interaction tests belong in describe('storage') > describe('namespace'). Dedicated describe('siteStorageNamespacing') unit tests added inside describe('v4') following the same pattern as other v4 flags. Co-Authored-By: Claude Opus 4.6 --- .../server/__tests__/configValidation.test.ts | 150 +++++++++++++----- 1 file changed, 112 insertions(+), 38 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 61069ebe0ee8..e88c61ac8cd1 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -1392,6 +1392,42 @@ describe('future', () => { ).toEqual(storageContaining({namespace: false})); }); + it('defaults namespace to true when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('defaults namespace to false when v4.siteStorageNamespacing is false', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: false}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace false even when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: false}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace string when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: 'custom'}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: 'custom'})); + }); + it('rejects namespace - null', () => { const storage: Partial = {namespace: null}; expect(() => @@ -1419,44 +1455,6 @@ describe('future', () => { }); }); - describe('siteStorageNamespacing', () => { - it('defaults namespace to true when siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: true})); - }); - - it('defaults namespace to false when siteStorageNamespacing is false', () => { - expect( - normalizeConfig({ - storage: {}, - future: {v4: {siteStorageNamespacing: false}}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('keeps explicit namespace false even when siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: false}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('keeps explicit namespace string when siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: 'custom'}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: 'custom'})); - }); - }); - describe('vcs', () => { function vcsContaining(vcs: Partial) { return futureContaining({ @@ -2507,6 +2505,7 @@ describe('future', () => { const v4: FutureV4Config = { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }; expect( normalizeConfig({ @@ -2697,5 +2696,80 @@ describe('future', () => { `); }); }); + + describe('siteStorageNamespacing', () => { + it('accepts - undefined', () => { + const v4: Partial = { + siteStorageNamespacing: undefined, + }; + expect( + normalizeConfig({ + future: { + v4, + }, + }), + ).toEqual(v4Containing({siteStorageNamespacing: false})); + }); + + it('accepts - true', () => { + const v4: Partial = { + siteStorageNamespacing: true, + }; + expect( + normalizeConfig({ + future: { + v4, + }, + }), + ).toEqual(v4Containing({siteStorageNamespacing: true})); + }); + + it('accepts - false', () => { + const v4: Partial = { + siteStorageNamespacing: false, + }; + expect( + normalizeConfig({ + future: { + v4, + }, + }), + ).toEqual(v4Containing({siteStorageNamespacing: false})); + }); + + it('rejects - null', () => { + const v4: Partial = { + // @ts-expect-error: invalid + siteStorageNamespacing: 42, + }; + expect(() => + normalizeConfig({ + future: { + v4, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.v4.siteStorageNamespacing" must be a boolean + " + `); + }); + + it('rejects - number', () => { + const v4: Partial = { + // @ts-expect-error: invalid + siteStorageNamespacing: 42, + }; + expect(() => + normalizeConfig({ + future: { + v4, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.v4.siteStorageNamespacing" must be a boolean + " + `); + }); + }); }); }); From 886e15867349c80c8e2a0975a11ccb9baf658eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 12 Mar 2026 15:47:10 +0100 Subject: [PATCH 5/6] Apply suggestion from @slorber --- .../docusaurus/src/server/__tests__/configValidation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index e88c61ac8cd1..f10f10700f02 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -2740,7 +2740,7 @@ describe('future', () => { it('rejects - null', () => { const v4: Partial = { // @ts-expect-error: invalid - siteStorageNamespacing: 42, + siteStorageNamespacing: null, }; expect(() => normalizeConfig({ From b05cdd35b64954532f25a38fbc61e952a77c5315 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 16:04:40 +0100 Subject: [PATCH 6/6] fix: move storage tests to top-level describe, fix null test - Storage tests are now a top-level describe('storage') block since storage is no longer under future config - storageContaining() helper moved back inside describe('storage') - Fix 'rejects - null' test to actually use null instead of 42 Co-Authored-By: Claude Opus 4.6 --- .../server/__tests__/configValidation.test.ts | 483 +++++++++--------- 1 file changed, 241 insertions(+), 242 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index f10f10700f02..8b9287b74882 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -1061,16 +1061,252 @@ describe('presets', () => { }); }); -describe('future', () => { - function futureContaining(future: Partial) { +describe('storage', () => { + function storageContaining(storage: Partial) { return expect.objectContaining({ - future: expect.objectContaining(future), + storage: expect.objectContaining(storage), }); } - function storageContaining(storage: Partial) { + it('accepts storage - undefined', () => { + expect( + normalizeConfig({ + storage: undefined, + }), + ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); + }); + + it('accepts storage - empty', () => { + expect( + normalizeConfig({ + storage: {}, + }), + ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); + }); + + it('accepts storage - full', () => { + const storage: StorageConfig = { + type: 'sessionStorage', + namespace: 'myNamespace', + }; + expect( + normalizeConfig({ + storage, + }), + ).toEqual(storageContaining(storage)); + }); + + it('rejects storage - boolean', () => { + // @ts-expect-error: invalid + const storage: Partial = true; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage" must be of type object + " + `); + }); + + it('rejects storage - number', () => { + // @ts-expect-error: invalid + const storage: Partial = 42; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage" must be of type object + " + `); + }); + + it('rejects future.experimental_storage', () => { + expect(() => + normalizeConfig({ + future: { + // @ts-expect-error: testing removed config + experimental_storage: { + type: 'sessionStorage', + namespace: true, + }, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "The Docusaurus config \`future.experimental_storage\` has been promoted to a stable top-level \`storage\` config attribute. Please move your storage config to the top level. + " + `); + }); + + describe('type', () => { + it('accepts type', () => { + const storage: Partial = { + type: 'sessionStorage', + }; + expect( + normalizeConfig({ + storage, + }), + ).toEqual( + storageContaining({ + ...DEFAULT_STORAGE_CONFIG, + ...storage, + }), + ); + }); + + it('accepts type - undefined', () => { + const storage: Partial = { + type: undefined, + }; + expect( + normalizeConfig({ + storage, + }), + ).toEqual(storageContaining({type: 'localStorage'})); + }); + + it('rejects type - null', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 42}; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage.type" must be one of [localStorage, sessionStorage] + "storage.type" must be a string + " + `); + }); + + it('rejects type - number', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 42}; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage.type" must be one of [localStorage, sessionStorage] + "storage.type" must be a string + " + `); + }); + + it('rejects type - invalid enum value', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 'badType'}; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage.type" must be one of [localStorage, sessionStorage] + " + `); + }); + }); + + describe('namespace', () => { + it('accepts namespace - boolean', () => { + const storage: Partial = { + namespace: true, + }; + expect( + normalizeConfig({ + storage, + }), + ).toEqual(storageContaining(storage)); + }); + + it('accepts namespace - string', () => { + const storage: Partial = { + namespace: 'myNamespace', + }; + expect( + normalizeConfig({ + storage, + }), + ).toEqual(storageContaining(storage)); + }); + + it('defaults namespace to false', () => { + expect( + normalizeConfig({ + storage: {}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('defaults namespace to true when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('defaults namespace to false when v4.siteStorageNamespacing is false', () => { + expect( + normalizeConfig({ + storage: {}, + future: {v4: {siteStorageNamespacing: false}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace false even when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: false}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('keeps explicit namespace string when v4.siteStorageNamespacing is true', () => { + expect( + normalizeConfig({ + storage: {namespace: 'custom'}, + future: {v4: {siteStorageNamespacing: true}}, + }), + ).toEqual(storageContaining({namespace: 'custom'})); + }); + + it('rejects namespace - null', () => { + const storage: Partial = {namespace: null}; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage.namespace" must be one of [string, boolean] + " + `); + }); + + it('rejects namespace - number', () => { + // @ts-expect-error: invalid + const storage: Partial = {namespace: 42}; + expect(() => + normalizeConfig({ + storage, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""storage.namespace" must be one of [string, boolean] + " + `); + }); + }); +}); + +describe('future', () => { + function futureContaining(future: Partial) { return expect.objectContaining({ - storage: expect.objectContaining(storage), + future: expect.objectContaining(future), }); } @@ -1219,242 +1455,6 @@ describe('future', () => { }); }); - describe('storage', () => { - it('accepts storage - undefined', () => { - expect( - normalizeConfig({ - storage: undefined, - }), - ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); - }); - - it('accepts storage - empty', () => { - expect( - normalizeConfig({ - storage: {}, - }), - ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); - }); - - it('accepts storage - full', () => { - const storage: StorageConfig = { - type: 'sessionStorage', - namespace: 'myNamespace', - }; - expect( - normalizeConfig({ - storage, - }), - ).toEqual(storageContaining(storage)); - }); - - it('rejects storage - boolean', () => { - // @ts-expect-error: invalid - const storage: Partial = true; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage" must be of type object - " - `); - }); - - it('rejects storage - number', () => { - // @ts-expect-error: invalid - const storage: Partial = 42; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage" must be of type object - " - `); - }); - - it('rejects future.experimental_storage', () => { - expect(() => - normalizeConfig({ - future: { - // @ts-expect-error: testing removed config - experimental_storage: { - type: 'sessionStorage', - namespace: true, - }, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - "The Docusaurus config \`future.experimental_storage\` has been promoted to a stable top-level \`storage\` config attribute. Please move your storage config to the top level. - " - `); - }); - - describe('type', () => { - it('accepts type', () => { - const storage: Partial = { - type: 'sessionStorage', - }; - expect( - normalizeConfig({ - storage, - }), - ).toEqual( - storageContaining({ - ...DEFAULT_STORAGE_CONFIG, - ...storage, - }), - ); - }); - - it('accepts type - undefined', () => { - const storage: Partial = { - type: undefined, - }; - expect( - normalizeConfig({ - storage, - }), - ).toEqual(storageContaining({type: 'localStorage'})); - }); - - it('rejects type - null', () => { - // @ts-expect-error: invalid - const storage: Partial = {type: 42}; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage.type" must be one of [localStorage, sessionStorage] - "storage.type" must be a string - " - `); - }); - - it('rejects type - number', () => { - // @ts-expect-error: invalid - const storage: Partial = {type: 42}; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage.type" must be one of [localStorage, sessionStorage] - "storage.type" must be a string - " - `); - }); - - it('rejects type - invalid enum value', () => { - // @ts-expect-error: invalid - const storage: Partial = {type: 'badType'}; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage.type" must be one of [localStorage, sessionStorage] - " - `); - }); - }); - - describe('namespace', () => { - it('accepts namespace - boolean', () => { - const storage: Partial = { - namespace: true, - }; - expect( - normalizeConfig({ - storage, - }), - ).toEqual(storageContaining(storage)); - }); - - it('accepts namespace - string', () => { - const storage: Partial = { - namespace: 'myNamespace', - }; - expect( - normalizeConfig({ - storage, - }), - ).toEqual(storageContaining(storage)); - }); - - it('defaults namespace to false', () => { - expect( - normalizeConfig({ - storage: {}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('defaults namespace to true when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: true})); - }); - - it('defaults namespace to false when v4.siteStorageNamespacing is false', () => { - expect( - normalizeConfig({ - storage: {}, - future: {v4: {siteStorageNamespacing: false}}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('keeps explicit namespace false even when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: false}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - it('keeps explicit namespace string when v4.siteStorageNamespacing is true', () => { - expect( - normalizeConfig({ - storage: {namespace: 'custom'}, - future: {v4: {siteStorageNamespacing: true}}, - }), - ).toEqual(storageContaining({namespace: 'custom'})); - }); - - it('rejects namespace - null', () => { - const storage: Partial = {namespace: null}; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage.namespace" must be one of [string, boolean] - " - `); - }); - - it('rejects namespace - number', () => { - // @ts-expect-error: invalid - const storage: Partial = {namespace: 42}; - expect(() => - normalizeConfig({ - storage, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""storage.namespace" must be one of [string, boolean] - " - `); - }); - }); - }); - describe('vcs', () => { function vcsContaining(vcs: Partial) { return futureContaining({ @@ -2739,7 +2739,6 @@ describe('future', () => { it('rejects - null', () => { const v4: Partial = { - // @ts-expect-error: invalid siteStorageNamespacing: null, }; expect(() =>