Skip to content

Commit 620e463

Browse files
authored
feat(core): site storage config options (experimental) (#10121)
1 parent cb68951 commit 620e463

File tree

20 files changed

+824
-54
lines changed

20 files changed

+824
-54
lines changed

packages/docusaurus-module-type-aliases/src/index.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ declare module '@generated/site-metadata' {
2626
export = siteMetadata;
2727
}
2828

29+
declare module '@generated/site-storage' {
30+
import type {SiteStorage} from '@docusaurus/types';
31+
32+
const siteStorage: SiteStorage;
33+
export = siteStorage;
34+
}
35+
2936
declare module '@generated/registry' {
3037
import type {Registry} from '@docusaurus/types';
3138

packages/docusaurus-theme-classic/src/index.ts

+50-41
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {createRequire} from 'module';
1010
import rtlcss from 'rtlcss';
1111
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
1212
import {getTranslationFiles, translateThemeConfig} from './translations';
13-
import type {LoadContext, Plugin} from '@docusaurus/types';
13+
import type {LoadContext, Plugin, SiteStorage} from '@docusaurus/types';
1414
import type {ThemeConfig} from '@docusaurus/theme-common';
1515
import type {Plugin as PostCssPlugin} from 'postcss';
1616
import type {PluginOptions} from '@docusaurus/theme-classic';
@@ -23,58 +23,66 @@ const ContextReplacementPlugin = requireFromDocusaurusCore(
2323
'webpack/lib/ContextReplacementPlugin',
2424
) as typeof webpack.ContextReplacementPlugin;
2525

26-
// Need to be inlined to prevent dark mode FOUC
27-
// Make sure the key is the same as the one in `/theme/hooks/useTheme.js`
28-
const ThemeStorageKey = 'theme';
2926
// Support for ?docusaurus-theme=dark
3027
const ThemeQueryStringKey = 'docusaurus-theme';
3128
// Support for ?docusaurus-data-mode=embed&docusaurus-data-myAttr=42
3229
const DataQueryStringPrefixKey = 'docusaurus-data-';
3330

3431
const noFlashColorMode = ({
35-
defaultMode,
36-
respectPrefersColorScheme,
37-
}: ThemeConfig['colorMode']) =>
32+
colorMode: {defaultMode, respectPrefersColorScheme},
33+
siteStorage,
34+
}: {
35+
colorMode: ThemeConfig['colorMode'];
36+
siteStorage: SiteStorage;
37+
}) => {
38+
// Need to be inlined to prevent dark mode FOUC
39+
// Make sure the key is the same as the one in the color mode React context
40+
// Currently defined in: `docusaurus-theme-common/src/contexts/colorMode.tsx`
41+
const themeStorageKey = `theme${siteStorage.namespace}`;
42+
3843
/* language=js */
39-
`(function() {
40-
var defaultMode = '${defaultMode}';
41-
var respectPrefersColorScheme = ${respectPrefersColorScheme};
44+
return `(function() {
45+
var defaultMode = '${defaultMode}';
46+
var respectPrefersColorScheme = ${respectPrefersColorScheme};
4247
43-
function setDataThemeAttribute(theme) {
44-
document.documentElement.setAttribute('data-theme', theme);
45-
}
48+
function setDataThemeAttribute(theme) {
49+
document.documentElement.setAttribute('data-theme', theme);
50+
}
4651
47-
function getQueryStringTheme() {
48-
try {
49-
return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}')
50-
} catch(e) {}
51-
}
52+
function getQueryStringTheme() {
53+
try {
54+
return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}')
55+
} catch (e) {
56+
}
57+
}
5258
53-
function getStoredTheme() {
54-
try {
55-
return localStorage.getItem('${ThemeStorageKey}');
56-
} catch (err) {}
57-
}
59+
function getStoredTheme() {
60+
try {
61+
return window['${siteStorage.type}'].getItem('${themeStorageKey}');
62+
} catch (err) {
63+
}
64+
}
5865
59-
var initialTheme = getQueryStringTheme() || getStoredTheme();
60-
if (initialTheme !== null) {
61-
setDataThemeAttribute(initialTheme);
62-
} else {
63-
if (
64-
respectPrefersColorScheme &&
65-
window.matchMedia('(prefers-color-scheme: dark)').matches
66-
) {
67-
setDataThemeAttribute('dark');
68-
} else if (
69-
respectPrefersColorScheme &&
70-
window.matchMedia('(prefers-color-scheme: light)').matches
71-
) {
72-
setDataThemeAttribute('light');
66+
var initialTheme = getQueryStringTheme() || getStoredTheme();
67+
if (initialTheme !== null) {
68+
setDataThemeAttribute(initialTheme);
7369
} else {
74-
setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light');
70+
if (
71+
respectPrefersColorScheme &&
72+
window.matchMedia('(prefers-color-scheme: dark)').matches
73+
) {
74+
setDataThemeAttribute('dark');
75+
} else if (
76+
respectPrefersColorScheme &&
77+
window.matchMedia('(prefers-color-scheme: light)').matches
78+
) {
79+
setDataThemeAttribute('light');
80+
} else {
81+
setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light');
82+
}
7583
}
76-
}
77-
})();`;
84+
})();`;
85+
};
7886

7987
/* language=js */
8088
const DataAttributeQueryStringInlineJavaScript = `
@@ -126,6 +134,7 @@ export default function themeClassic(
126134
): Plugin<undefined> {
127135
const {
128136
i18n: {currentLocale, localeConfigs},
137+
siteStorage,
129138
} = context;
130139
const themeConfig = context.siteConfig.themeConfig as ThemeConfig;
131140
const {
@@ -218,7 +227,7 @@ export default function themeClassic(
218227
{
219228
tagName: 'script',
220229
innerHTML: `
221-
${noFlashColorMode(colorMode)}
230+
${noFlashColorMode({colorMode, siteStorage})}
222231
${DataAttributeQueryStringInlineJavaScript}
223232
${announcementBar ? AnnouncementBarInlineJavaScript : ''}
224233
`,

packages/docusaurus-theme-common/src/utils/storageUtils.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
*/
77

88
import {useCallback, useRef, useSyncExternalStore} from 'react';
9+
import SiteStorage from '@generated/site-storage';
910

10-
const StorageTypes = ['localStorage', 'sessionStorage', 'none'] as const;
11+
export type StorageType = (typeof SiteStorage)['type'] | 'none';
1112

12-
export type StorageType = (typeof StorageTypes)[number];
13+
const DefaultStorageType: StorageType = SiteStorage.type;
1314

14-
const DefaultStorageType: StorageType = 'localStorage';
15+
function applyNamespace(storageKey: string): string {
16+
return `${storageKey}${SiteStorage.namespace}`;
17+
}
1518

1619
// window.addEventListener('storage') only works for different windows...
1720
// so for current window we have to dispatch the event manually
@@ -134,9 +137,10 @@ Please only call storage APIs in effects and event handlers.`);
134137
* this API can be a no-op. See also https://github.com/facebook/docusaurus/issues/6036
135138
*/
136139
export function createStorageSlot(
137-
key: string,
140+
keyInput: string,
138141
options?: {persistence?: StorageType},
139142
): StorageSlot {
143+
const key = applyNamespace(keyInput);
140144
if (typeof window === 'undefined') {
141145
return createServerStorageSlot(key);
142146
}

packages/docusaurus-types/src/config.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import type {SiteStorage} from './context';
89
import type {RuleSetRule} from 'webpack';
910
import type {Required as RequireKeys, DeepPartial} from 'utility-types';
1011
import type {I18nConfig} from './i18n';
@@ -115,6 +116,15 @@ export type MarkdownConfig = {
115116
anchors: MarkdownAnchorsConfig;
116117
};
117118

119+
export type StorageConfig = {
120+
type: SiteStorage['type'];
121+
namespace: boolean | string;
122+
};
123+
124+
export type FutureConfig = {
125+
experimental_storage: StorageConfig;
126+
};
127+
118128
/**
119129
* Docusaurus config, after validation/normalization.
120130
*/
@@ -171,6 +181,11 @@ export type DocusaurusConfig = {
171181
* @see https://docusaurus.io/docs/api/docusaurus-config#i18n
172182
*/
173183
i18n: I18nConfig;
184+
/**
185+
* Docusaurus future flags and experimental features.
186+
* Similar to Remix future flags, see https://remix.run/blog/future-flags
187+
*/
188+
future: FutureConfig;
174189
/**
175190
* This option adds `<meta name="robots" content="noindex, nofollow">` to
176191
* every page to tell search engines to avoid indexing your site.

packages/docusaurus-types/src/context.d.ts

+24
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ export type SiteMetadata = {
2727
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
2828
};
2929

30+
export type SiteStorage = {
31+
/**
32+
* Which browser storage do you want to use?
33+
* Between "localStorage" and "sessionStorage".
34+
* The default is "localStorage".
35+
*/
36+
type: 'localStorage' | 'sessionStorage';
37+
38+
/**
39+
* Applies a namespace to the theme storage key
40+
* For readability, the namespace is applied at the end of the key
41+
* The final storage key will be = `${key}${namespace}`
42+
*
43+
* The default namespace is "" for retro-compatibility reasons
44+
* If you want a separator, the namespace should contain it ("-myNamespace")
45+
*/
46+
namespace: string;
47+
};
48+
3049
export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
3150

3251
export type LoadContext = {
@@ -50,6 +69,11 @@ export type LoadContext = {
5069
baseUrl: string;
5170
i18n: I18n;
5271
codeTranslations: CodeTranslations;
72+
73+
/**
74+
* Defines the default browser storage behavior for a site
75+
*/
76+
siteStorage: SiteStorage;
5377
};
5478

5579
export type Props = LoadContext & {

packages/docusaurus-types/src/index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export {
1212
DefaultParseFrontMatter,
1313
ParseFrontMatter,
1414
DocusaurusConfig,
15+
FutureConfig,
16+
StorageConfig,
1517
Config,
1618
} from './config';
1719

@@ -20,6 +22,7 @@ export {
2022
DocusaurusContext,
2123
GlobalData,
2224
LoadContext,
25+
SiteStorage,
2326
Props,
2427
} from './context';
2528

packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap

+60
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
77
"baseUrlIssueBanner": true,
88
"clientModules": [],
99
"customFields": {},
10+
"future": {
11+
"experimental_storage": {
12+
"namespace": false,
13+
"type": "localStorage",
14+
},
15+
},
1016
"headTags": [],
1117
"i18n": {
1218
"defaultLocale": "en",
@@ -61,6 +67,12 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
6167
"baseUrlIssueBanner": true,
6268
"clientModules": [],
6369
"customFields": {},
70+
"future": {
71+
"experimental_storage": {
72+
"namespace": false,
73+
"type": "localStorage",
74+
},
75+
},
6476
"headTags": [],
6577
"i18n": {
6678
"defaultLocale": "en",
@@ -115,6 +127,12 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
115127
"baseUrlIssueBanner": true,
116128
"clientModules": [],
117129
"customFields": {},
130+
"future": {
131+
"experimental_storage": {
132+
"namespace": false,
133+
"type": "localStorage",
134+
},
135+
},
118136
"headTags": [],
119137
"i18n": {
120138
"defaultLocale": "en",
@@ -169,6 +187,12 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
169187
"baseUrlIssueBanner": true,
170188
"clientModules": [],
171189
"customFields": {},
190+
"future": {
191+
"experimental_storage": {
192+
"namespace": false,
193+
"type": "localStorage",
194+
},
195+
},
172196
"headTags": [],
173197
"i18n": {
174198
"defaultLocale": "en",
@@ -223,6 +247,12 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
223247
"baseUrlIssueBanner": true,
224248
"clientModules": [],
225249
"customFields": {},
250+
"future": {
251+
"experimental_storage": {
252+
"namespace": false,
253+
"type": "localStorage",
254+
},
255+
},
226256
"headTags": [],
227257
"i18n": {
228258
"defaultLocale": "en",
@@ -277,6 +307,12 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
277307
"baseUrlIssueBanner": true,
278308
"clientModules": [],
279309
"customFields": {},
310+
"future": {
311+
"experimental_storage": {
312+
"namespace": false,
313+
"type": "localStorage",
314+
},
315+
},
280316
"headTags": [],
281317
"i18n": {
282318
"defaultLocale": "en",
@@ -331,6 +367,12 @@ exports[`loadSiteConfig website with valid async config 1`] = `
331367
"baseUrlIssueBanner": true,
332368
"clientModules": [],
333369
"customFields": {},
370+
"future": {
371+
"experimental_storage": {
372+
"namespace": false,
373+
"type": "localStorage",
374+
},
375+
},
334376
"headTags": [],
335377
"i18n": {
336378
"defaultLocale": "en",
@@ -387,6 +429,12 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
387429
"baseUrlIssueBanner": true,
388430
"clientModules": [],
389431
"customFields": {},
432+
"future": {
433+
"experimental_storage": {
434+
"namespace": false,
435+
"type": "localStorage",
436+
},
437+
},
390438
"headTags": [],
391439
"i18n": {
392440
"defaultLocale": "en",
@@ -443,6 +491,12 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
443491
"baseUrlIssueBanner": true,
444492
"clientModules": [],
445493
"customFields": {},
494+
"future": {
495+
"experimental_storage": {
496+
"namespace": false,
497+
"type": "localStorage",
498+
},
499+
},
446500
"headTags": [],
447501
"i18n": {
448502
"defaultLocale": "en",
@@ -502,6 +556,12 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
502556
],
503557
"customFields": {},
504558
"favicon": "img/docusaurus.ico",
559+
"future": {
560+
"experimental_storage": {
561+
"namespace": false,
562+
"type": "localStorage",
563+
},
564+
},
505565
"headTags": [],
506566
"i18n": {
507567
"defaultLocale": "en",

0 commit comments

Comments
 (0)