From e1dc8831ab5f5cbf2d505c416d5652bc72d4bc05 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 12:49:57 +0100 Subject: [PATCH 1/9] Mark storage API stable and add v4 siteStorageNamespacing flag (AI-assisted) --- packages/docusaurus-types/src/config.d.ts | 10 +- .../__snapshots__/config.test.ts.snap | 90 ++++---- .../__tests__/__snapshots__/site.test.ts.snap | 108 +++++---- .../server/__tests__/configValidation.test.ts | 213 +++++++++++++----- .../src/server/__tests__/storage.test.ts | 37 ++- .../docusaurus/src/server/configValidation.ts | 43 +++- packages/docusaurus/src/server/storage.ts | 25 +- website/docs/api/docusaurus.config.js.mdx | 33 ++- website/docusaurus.config.ts | 6 +- 9 files changed, 388 insertions(+), 177 deletions(-) diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index bc4180ca7b37..cc70ef21d218 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; /** @@ -180,6 +179,13 @@ export type DocusaurusConfig = { * Similar to Remix future flags, see https://remix.run/blog/future-flags */ future: FutureConfig; + /** + * Site-wide browser storage options that theme authors should strive to + * respect. + * + * @see https://docusaurus.io/docs/api/docusaurus-config#storage + */ + storage: StorageConfig; /** * This option adds `` to * every page to tell search engines to avoid indexing your site. 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..63576a2a0ae7 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -61,6 +61,7 @@ describe('normalizeConfig', () => { v4: { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }, experimental_faster: { swcJsLoader: true, @@ -73,10 +74,6 @@ describe('normalizeConfig', () => { ssgWorkerThreads: true, gitEagerVcs: true, }, - experimental_storage: { - type: 'sessionStorage', - namespace: true, - }, experimental_vcs: { initialize: (_params) => {}, getFileCreationInfo: (_filePath) => null, @@ -84,6 +81,10 @@ describe('normalizeConfig', () => { }, experimental_router: 'hash', }, + storage: { + type: 'sessionStorage', + namespace: true, + }, tagline: 'my awesome site', organizationName: 'facebook', projectName: 'docusaurus', @@ -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,25 @@ 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(storageContaining(DEFAULT_STORAGE_CONFIG)); }); it('accepts storage - empty', () => { expect( normalizeConfig({ - future: {experimental_storage: {}}, + storage: {}, }), - ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG)); + ).toEqual(storageContaining(DEFAULT_STORAGE_CONFIG)); }); it('accepts storage - full', () => { @@ -1247,9 +1243,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); @@ -1259,12 +1253,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,15 +1264,28 @@ 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 legacy future.experimental_storage', () => { expect(() => normalizeConfig({ future: { - experimental_storage: storage, + // @ts-expect-error: legacy config removed + experimental_storage: {namespace: true}, }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_storage" must be of type object - " + "Docusaurus config \`future.experimental_storage\` was moved and is no longer supported. + Please use the top-level \`storage\` option instead. + See https://docusaurus.io/docs/api/docusaurus-config#storage" `); }); @@ -1291,9 +1296,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual( storageContaining({ @@ -1309,9 +1312,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining({type: 'localStorage'})); }); @@ -1321,13 +1322,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 +1336,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,27 +1350,53 @@ 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] " `); }); }); describe('namespace', () => { + it('defaults to true when future.v4.siteStorageNamespacing is enabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: true, + }, + }, + storage: { + type: 'localStorage', + }, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('respects explicit namespace when future.v4.siteStorageNamespacing is enabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: true, + }, + }, + storage: { + namespace: false, + }, + }), + ).toEqual(storageContaining({namespace: false})); + }); + it('accepts namespace - boolean', () => { const storage: Partial = { namespace: true, }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); @@ -1384,9 +1407,7 @@ describe('future', () => { }; expect( normalizeConfig({ - future: { - experimental_storage: storage, - }, + storage, }), ).toEqual(storageContaining(storage)); }); @@ -1395,12 +1416,10 @@ describe('future', () => { 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 +1429,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] " `); }); @@ -2472,6 +2489,7 @@ describe('future', () => { const v4: FutureV4Config = { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }; expect( normalizeConfig({ @@ -2662,5 +2680,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 + " + `); + }); + }); }); }); diff --git a/packages/docusaurus/src/server/__tests__/storage.test.ts b/packages/docusaurus/src/server/__tests__/storage.test.ts index f91b9ddbef3e..4cd146800927 100644 --- a/packages/docusaurus/src/server/__tests__/storage.test.ts +++ b/packages/docusaurus/src/server/__tests__/storage.test.ts @@ -8,6 +8,7 @@ import {createSiteStorage} from '../storage'; import { DEFAULT_FUTURE_CONFIG, + DEFAULT_FUTURE_V4_CONFIG, DEFAULT_STORAGE_CONFIG, } from '../configValidation'; import type {FutureConfig, StorageConfig, SiteStorage} from '@docusaurus/types'; @@ -16,20 +17,30 @@ function test({ url = 'https://docusaurus.io', baseUrl = '/', storage = {}, + siteStorageNamespacing = DEFAULT_FUTURE_V4_CONFIG.siteStorageNamespacing, }: { url?: string; baseUrl?: string; storage?: Partial; + siteStorageNamespacing?: boolean; }): SiteStorage { const future: FutureConfig = { ...DEFAULT_FUTURE_CONFIG, - experimental_storage: { - ...DEFAULT_STORAGE_CONFIG, - ...storage, + v4: { + ...DEFAULT_FUTURE_V4_CONFIG, + siteStorageNamespacing, }, }; - return createSiteStorage({url, baseUrl, future}); + return createSiteStorage({ + url, + baseUrl, + future, + storage: { + type: DEFAULT_STORAGE_CONFIG.type, + ...storage, + }, + }); } const DefaultSiteStorage: SiteStorage = { @@ -42,6 +53,17 @@ describe('storage', () => { expect(test({})).toEqual(DefaultSiteStorage); }); + it('defaults namespace to true when future.v4.siteStorageNamespacing is enabled', () => { + expect( + test({ + siteStorageNamespacing: true, + }), + ).toEqual({ + ...DefaultSiteStorage, + namespace: '-189', + }); + }); + describe('type', () => { it('localStorage', () => { expect(test({storage: {type: 'localStorage'}})).toEqual({ @@ -149,7 +171,12 @@ describe('storage', () => { }); it('false', () => { - expect(test({storage: {namespace: false}})).toEqual({ + expect( + test({ + storage: {namespace: false}, + siteStorageNamespacing: true, + }), + ).toEqual({ ...DefaultSiteStorage, namespace: '', }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index e57a7ba45abe..b446e006f2dd 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', }; @@ -143,6 +144,7 @@ export const DEFAULT_CONFIG: Pick< DocusaurusConfig, | 'i18n' | 'future' + | 'storage' | 'onBrokenLinks' | 'onBrokenAnchors' | 'onBrokenMarkdownLinks' @@ -165,6 +167,7 @@ export const DEFAULT_CONFIG: Pick< > = { i18n: DEFAULT_I18N_CONFIG, future: DEFAULT_FUTURE_CONFIG, + storage: DEFAULT_STORAGE_CONFIG, onBrokenLinks: 'throw', onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw onBrokenMarkdownLinks: undefined, @@ -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,10 @@ 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: 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,7 +376,6 @@ 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') @@ -391,6 +394,7 @@ export const ConfigSchema = Joi.object({ trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior! i18n: I18N_CONFIG_SCHEMA, future: FUTURE_CONFIG_SCHEMA, + storage: STORAGE_CONFIG_SCHEMA, onBrokenLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenLinks), @@ -533,6 +537,10 @@ 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) { + 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.`; @@ -584,11 +592,34 @@ All the v4 future flags are documented here: https://docusaurus.io/docs/api/docu } } +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function hasLegacyExperimentalStorageConfig(config: unknown): boolean { + if (!isObject(config)) { + return false; + } + const {future} = config; + return ( + isObject(future) && + Object.prototype.hasOwnProperty.call(future, 'experimental_storage') + ); +} + // TODO move to @docusaurus/utils-validation export function validateConfig( config: unknown, siteConfigPath: string, ): DocusaurusConfig { + if (hasLegacyExperimentalStorageConfig(config)) { + throw new Error(`Docusaurus config ${logger.code( + 'future.experimental_storage', + )} was moved and is no longer supported. +Please use the top-level ${logger.code('storage')} option instead. +See https://docusaurus.io/docs/api/docusaurus-config#storage`); + } + const {error, warning, value} = ConfigSchema.validate(config, { abortEarly: false, }); diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 657f4a359c2d..89e5d2729087 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -7,12 +7,23 @@ import {normalizeUrl, simpleHash} from '@docusaurus/utils'; import {addTrailingSlash} from '@docusaurus/utils-common'; -import type {DocusaurusConfig, SiteStorage} from '@docusaurus/types'; +import type { + DocusaurusConfig, + SiteStorage, + StorageConfig, +} from '@docusaurus/types'; -type PartialFuture = Pick; +type PartialFuture = { + v4: Pick; +}; + +type PartialStorage = Pick & { + namespace?: StorageConfig['namespace']; +}; type PartialConfig = Pick & { future: PartialFuture; + storage: PartialStorage; }; function automaticNamespace(config: PartialConfig): string { @@ -23,17 +34,19 @@ function automaticNamespace(config: PartialConfig): string { } function getNamespaceString(config: PartialConfig): string | null { - if (config.future.experimental_storage.namespace === true) { + const namespace = config.storage.namespace ?? config.future.v4.siteStorageNamespacing; + + if (namespace === true) { return automaticNamespace(config); - } else if (config.future.experimental_storage.namespace === false) { + } else if (namespace === false) { return null; } else { - return config.future.experimental_storage.namespace; + return 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..19b45e2e999f 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -234,6 +234,7 @@ export default { v4: { removeLegacyPostBuildHeadAttribute: true, useCssCascadeLayers: true, + siteStorageNamespacing: true, }, experimental_faster: { swcJsLoader: true, @@ -245,18 +246,19 @@ export default { ssgWorkerThreads: true, mdxCrossCompilerCache: true, }, - experimental_storage: { - type: 'localStorage', - namespace: true, - }, experimental_router: 'hash', }, + storage: { + type: 'localStorage', + namespace: true, + }, }; ``` - `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`: Changes the default value of [`storage.namespace`](#storage) to `true`. This helps you prepare for the upcoming Docusaurus v4 default behavior. - `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 +269,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. @@ -361,6 +360,26 @@ type VcsPreset = | 'default-v2'; ``` +### `storage` {/* #storage */} + +- Type: `Object` + +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` (historical behavior), unless `future.v4.siteStorageNamespacing` is turned on. + +Example: + +```js title="docusaurus.config.js" +export default { + storage: { + type: 'localStorage', + namespace: true, + }, +}; +``` + ### `noIndex` {/* #noIndex */} - Type: `boolean` diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index f242e5aba33d..ef57df8f5e77 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -193,12 +193,12 @@ export default async function createConfigAsync() { ssgWorkerThreads: true, gitEagerVcs: true, }, - experimental_storage: { - namespace: true, - }, experimental_vcs: vcs, experimental_router: router, }, + storage: { + namespace: true, + }, // Dogfood both settings: // - force trailing slashes for deploy previews // - avoid trailing slashes in prod From 243bfd306bbc9b3c30c0654daccfaf747955f364 Mon Sep 17 00:00:00 2001 From: slorber <749374+slorber@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:15:10 +0000 Subject: [PATCH 2/9] refactor: apply lint autofix --- packages/docusaurus/src/server/storage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 89e5d2729087..8aed2917072c 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -34,7 +34,8 @@ function automaticNamespace(config: PartialConfig): string { } function getNamespaceString(config: PartialConfig): string | null { - const namespace = config.storage.namespace ?? config.future.v4.siteStorageNamespacing; + const namespace = + config.storage.namespace ?? config.future.v4.siteStorageNamespacing; if (namespace === true) { return automaticNamespace(config); From 8d08145898b838ffc0482ce2f678762fad02b4f1 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 14:24:29 +0100 Subject: [PATCH 3/9] chore: trigger CI (AI-assisted) From 8aa171033ff4274219180a1e2c6f5b26ab3d7582 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 14:46:09 +0100 Subject: [PATCH 4/9] Address PR review feedback on storage promotion (AI-assisted) --- .../server/__tests__/configValidation.test.ts | 7 ++-- .../src/server/__tests__/storage.test.ts | 39 ++----------------- .../docusaurus/src/server/configValidation.ts | 33 +++++----------- packages/docusaurus/src/server/storage.ts | 20 ++-------- website/docs/api/docusaurus.config.js.mdx | 4 -- website/docusaurus.config.ts | 3 -- 6 files changed, 21 insertions(+), 85 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 63576a2a0ae7..c7d58ea018f4 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -1283,9 +1283,10 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - "Docusaurus config \`future.experimental_storage\` was moved and is no longer supported. + "Docusaurus config \`future.experimental_storage\` has been promoted to stable and is no longer supported. Please use the top-level \`storage\` option instead. - See https://docusaurus.io/docs/api/docusaurus-config#storage" + See https://docusaurus.io/docs/api/docusaurus-config#storage + " `); }); @@ -2724,7 +2725,7 @@ describe('future', () => { it('rejects - null', () => { const v4: Partial = { // @ts-expect-error: invalid - siteStorageNamespacing: 42, + siteStorageNamespacing: null, }; expect(() => normalizeConfig({ diff --git a/packages/docusaurus/src/server/__tests__/storage.test.ts b/packages/docusaurus/src/server/__tests__/storage.test.ts index 4cd146800927..57999a6dc454 100644 --- a/packages/docusaurus/src/server/__tests__/storage.test.ts +++ b/packages/docusaurus/src/server/__tests__/storage.test.ts @@ -6,38 +6,23 @@ */ import {createSiteStorage} from '../storage'; -import { - DEFAULT_FUTURE_CONFIG, - DEFAULT_FUTURE_V4_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', baseUrl = '/', storage = {}, - siteStorageNamespacing = DEFAULT_FUTURE_V4_CONFIG.siteStorageNamespacing, }: { url?: string; baseUrl?: string; storage?: Partial; - siteStorageNamespacing?: boolean; }): SiteStorage { - const future: FutureConfig = { - ...DEFAULT_FUTURE_CONFIG, - v4: { - ...DEFAULT_FUTURE_V4_CONFIG, - siteStorageNamespacing, - }, - }; - return createSiteStorage({ url, baseUrl, - future, storage: { - type: DEFAULT_STORAGE_CONFIG.type, + ...DEFAULT_STORAGE_CONFIG, ...storage, }, }); @@ -53,17 +38,6 @@ describe('storage', () => { expect(test({})).toEqual(DefaultSiteStorage); }); - it('defaults namespace to true when future.v4.siteStorageNamespacing is enabled', () => { - expect( - test({ - siteStorageNamespacing: true, - }), - ).toEqual({ - ...DefaultSiteStorage, - namespace: '-189', - }); - }); - describe('type', () => { it('localStorage', () => { expect(test({storage: {type: 'localStorage'}})).toEqual({ @@ -171,12 +145,7 @@ describe('storage', () => { }); it('false', () => { - expect( - test({ - storage: {namespace: false}, - siteStorageNamespacing: true, - }), - ).toEqual({ + expect(test({storage: {namespace: false}})).toEqual({ ...DefaultSiteStorage, namespace: '', }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index b446e006f2dd..8ddec29ba0f6 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -338,6 +338,7 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({ type: Joi.string() .equal('localStorage', 'sessionStorage') .default(DEFAULT_STORAGE_CONFIG.type), + // No schema default on purpose: we derive this from future.v4 flag later. namespace: Joi.alternatives().try(Joi.string(), Joi.boolean()), }) .optional() @@ -376,6 +377,15 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, + experimental_storage: Joi.any() + .forbidden() + .messages({ + 'any.unknown': `Docusaurus config ${logger.code( + 'future.experimental_storage', + )} has been promoted to stable and is no longer supported. +Please use the top-level ${logger.code('storage')} option instead. +See https://docusaurus.io/docs/api/docusaurus-config#storage`, + }), experimental_vcs: VCS_CONFIG_SCHEMA, experimental_router: Joi.string() .equal('browser', 'hash') @@ -592,34 +602,11 @@ All the v4 future flags are documented here: https://docusaurus.io/docs/api/docu } } -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -function hasLegacyExperimentalStorageConfig(config: unknown): boolean { - if (!isObject(config)) { - return false; - } - const {future} = config; - return ( - isObject(future) && - Object.prototype.hasOwnProperty.call(future, 'experimental_storage') - ); -} - // TODO move to @docusaurus/utils-validation export function validateConfig( config: unknown, siteConfigPath: string, ): DocusaurusConfig { - if (hasLegacyExperimentalStorageConfig(config)) { - throw new Error(`Docusaurus config ${logger.code( - 'future.experimental_storage', - )} was moved and is no longer supported. -Please use the top-level ${logger.code('storage')} option instead. -See https://docusaurus.io/docs/api/docusaurus-config#storage`); - } - const {error, warning, value} = ConfigSchema.validate(config, { abortEarly: false, }); diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 8aed2917072c..877528f3b8a9 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -7,23 +7,10 @@ import {normalizeUrl, simpleHash} from '@docusaurus/utils'; import {addTrailingSlash} from '@docusaurus/utils-common'; -import type { - DocusaurusConfig, - SiteStorage, - StorageConfig, -} from '@docusaurus/types'; - -type PartialFuture = { - v4: Pick; -}; - -type PartialStorage = Pick & { - namespace?: StorageConfig['namespace']; -}; +import type {DocusaurusConfig, SiteStorage} from '@docusaurus/types'; type PartialConfig = Pick & { - future: PartialFuture; - storage: PartialStorage; + storage: DocusaurusConfig['storage']; }; function automaticNamespace(config: PartialConfig): string { @@ -34,8 +21,7 @@ function automaticNamespace(config: PartialConfig): string { } function getNamespaceString(config: PartialConfig): string | null { - const namespace = - config.storage.namespace ?? config.future.v4.siteStorageNamespacing; + const {namespace} = config.storage; if (namespace === true) { return automaticNamespace(config); diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 19b45e2e999f..deec583c530b 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -248,10 +248,6 @@ export default { }, experimental_router: 'hash', }, - storage: { - type: 'localStorage', - namespace: true, - }, }; ``` diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index ef57df8f5e77..f49ef0c0247b 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -196,9 +196,6 @@ export default async function createConfigAsync() { experimental_vcs: vcs, experimental_router: router, }, - storage: { - namespace: true, - }, // Dogfood both settings: // - force trailing slashes for deploy previews // - avoid trailing slashes in prod From dc8684e0d79419773fee09d7a2d5533d3ec68977 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 15:39:10 +0100 Subject: [PATCH 5/9] Fix Joi typing for removed future.experimental_storage (AI-assisted) --- .../docusaurus/src/server/configValidation.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 8ddec29ba0f6..4e612e4f0aed 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -377,20 +377,23 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, - experimental_storage: Joi.any() - .forbidden() - .messages({ - 'any.unknown': `Docusaurus config ${logger.code( - 'future.experimental_storage', - )} has been promoted to stable and is no longer supported. -Please use the top-level ${logger.code('storage')} option instead. -See https://docusaurus.io/docs/api/docusaurus-config#storage`, - }), experimental_vcs: VCS_CONFIG_SCHEMA, experimental_router: Joi.string() .equal('browser', 'hash') .default(DEFAULT_FUTURE_CONFIG.experimental_router), }) + .pattern( + /^experimental_storage$/, + Joi.any() + .forbidden() + .messages({ + 'any.unknown': `Docusaurus config ${logger.code( + 'future.experimental_storage', + )} has been promoted to stable and is no longer supported. +Please use the top-level ${logger.code('storage')} option instead. +See https://docusaurus.io/docs/api/docusaurus-config#storage`, + }), + ) .optional() .default(DEFAULT_FUTURE_CONFIG); From 6fbdf1b547836952ae684cd4faedec626cb64154 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 15:46:00 +0100 Subject: [PATCH 6/9] refactor(core): simplify storage partial config typing (AI-assisted) --- packages/docusaurus/src/server/storage.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 877528f3b8a9..86ed8a2c1822 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -9,9 +9,7 @@ import {normalizeUrl, simpleHash} from '@docusaurus/utils'; import {addTrailingSlash} from '@docusaurus/utils-common'; import type {DocusaurusConfig, SiteStorage} from '@docusaurus/types'; -type PartialConfig = Pick & { - storage: DocusaurusConfig['storage']; -}; +type PartialConfig = Pick; function automaticNamespace(config: PartialConfig): string { const normalizedUrl = addTrailingSlash( From c4dd8cecd3fdf4007137444a674192ca68b21657 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 15:46:42 +0100 Subject: [PATCH 7/9] refactor(core): replace Joi pattern workaround for legacy storage key (AI-assisted) --- .../docusaurus/src/server/configValidation.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 4e612e4f0aed..2ffc47b1d232 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -374,26 +374,27 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { return value; }).default(true); -const FUTURE_CONFIG_SCHEMA = Joi.object({ +type FutureConfigWithLegacyStorage = FutureConfig & { + experimental_storage?: never; +}; + +const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, experimental_faster: FASTER_CONFIG_SCHEMA, + experimental_storage: Joi.any() + .forbidden() + .messages({ + 'any.unknown': `Docusaurus config ${logger.code( + 'future.experimental_storage', + )} has been promoted to stable and is no longer supported. +Please use the top-level ${logger.code('storage')} option instead. +See https://docusaurus.io/docs/api/docusaurus-config#storage`, + }), experimental_vcs: VCS_CONFIG_SCHEMA, experimental_router: Joi.string() .equal('browser', 'hash') .default(DEFAULT_FUTURE_CONFIG.experimental_router), }) - .pattern( - /^experimental_storage$/, - Joi.any() - .forbidden() - .messages({ - 'any.unknown': `Docusaurus config ${logger.code( - 'future.experimental_storage', - )} has been promoted to stable and is no longer supported. -Please use the top-level ${logger.code('storage')} option instead. -See https://docusaurus.io/docs/api/docusaurus-config#storage`, - }), - ) .optional() .default(DEFAULT_FUTURE_CONFIG); From 3c96284c5bcbec01eeea03bd0c513383971f32fb Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 16:11:40 +0100 Subject: [PATCH 8/9] refactor(core): inline future legacy storage typing and simplify namespace lookup (AI-assisted) --- packages/docusaurus/src/server/configValidation.ts | 8 +++----- packages/docusaurus/src/server/storage.ts | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 2ffc47b1d232..6ccde0034917 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -374,11 +374,9 @@ const VCS_CONFIG_SCHEMA = Joi.custom((input) => { return value; }).default(true); -type FutureConfigWithLegacyStorage = FutureConfig & { - experimental_storage?: never; -}; - -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_storage: Joi.any() diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts index 86ed8a2c1822..932db7df2695 100644 --- a/packages/docusaurus/src/server/storage.ts +++ b/packages/docusaurus/src/server/storage.ts @@ -19,14 +19,12 @@ function automaticNamespace(config: PartialConfig): string { } function getNamespaceString(config: PartialConfig): string | null { - const {namespace} = config.storage; - - if (namespace === true) { + if (config.storage.namespace === true) { return automaticNamespace(config); - } else if (namespace === false) { + } else if (config.storage.namespace === false) { return null; } else { - return namespace; + return config.storage.namespace; } } From dabe71aeb9320d44f0b4cefa6ad43ee918273bc5 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Mar 2026 16:11:48 +0100 Subject: [PATCH 9/9] test(core): move storage config tests out of future and cover namespacing defaults (AI-assisted) --- .../server/__tests__/configValidation.test.ts | 524 ++++++++++-------- 1 file changed, 297 insertions(+), 227 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index c7d58ea018f4..42475f8bc26a 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -1061,6 +1061,303 @@ describe('presets', () => { }); }); +describe('storage', () => { + function storageContaining(storage: Partial) { + return expect.objectContaining({ + storage: expect.objectContaining(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 legacy future.experimental_storage', () => { + expect(() => + normalizeConfig({ + future: { + // @ts-expect-error: legacy config removed + experimental_storage: {namespace: true}, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Docusaurus config \`future.experimental_storage\` has been promoted to stable and is no longer supported. + Please use the top-level \`storage\` option instead. + See https://docusaurus.io/docs/api/docusaurus-config#storage + " + `); + }); + + 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('defaults to true when future.v4.siteStorageNamespacing is enabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: true, + }, + }, + storage: { + type: 'localStorage', + }, + }), + ).toEqual(storageContaining({namespace: true})); + }); + + it('defaults to false when future.v4.siteStorageNamespacing is disabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: false, + }, + }, + storage: { + type: 'localStorage', + }, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('defaults to false when namespace is not provided', () => { + expect( + normalizeConfig({ + storage: { + type: 'localStorage', + }, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('respects explicit namespace when future.v4.siteStorageNamespacing is enabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: true, + }, + }, + storage: { + namespace: false, + }, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('respects explicit namespace when future.v4.siteStorageNamespacing is disabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: false, + }, + }, + storage: { + namespace: false, + }, + }), + ).toEqual(storageContaining({namespace: false})); + }); + + it('respects explicit string namespace when future.v4.siteStorageNamespacing is enabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: true, + }, + }, + storage: { + namespace: 'myNamespace', + }, + }), + ).toEqual(storageContaining({namespace: 'myNamespace'})); + }); + + it('respects explicit string namespace when future.v4.siteStorageNamespacing is disabled', () => { + expect( + normalizeConfig({ + future: { + v4: { + siteStorageNamespacing: false, + }, + }, + storage: { + namespace: 'myNamespace', + }, + }), + ).toEqual(storageContaining({namespace: 'myNamespace'})); + }); + + 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('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({ @@ -1213,233 +1510,6 @@ describe('future', () => { }); }); - describe('storage', () => { - function storageContaining(storage: Partial) { - return expect.objectContaining({ - storage: expect.objectContaining(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 legacy future.experimental_storage', () => { - expect(() => - normalizeConfig({ - future: { - // @ts-expect-error: legacy config removed - experimental_storage: {namespace: true}, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - "Docusaurus config \`future.experimental_storage\` has been promoted to stable and is no longer supported. - Please use the top-level \`storage\` option instead. - See https://docusaurus.io/docs/api/docusaurus-config#storage - " - `); - }); - - 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('defaults to true when future.v4.siteStorageNamespacing is enabled', () => { - expect( - normalizeConfig({ - future: { - v4: { - siteStorageNamespacing: true, - }, - }, - storage: { - type: 'localStorage', - }, - }), - ).toEqual(storageContaining({namespace: true})); - }); - - it('respects explicit namespace when future.v4.siteStorageNamespacing is enabled', () => { - expect( - normalizeConfig({ - future: { - v4: { - siteStorageNamespacing: true, - }, - }, - storage: { - namespace: false, - }, - }), - ).toEqual(storageContaining({namespace: false})); - }); - - 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('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({