mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
264 lines
7.4 KiB
TypeScript
264 lines
7.4 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {useCallback, useRef, useSyncExternalStore} from 'react';
|
|
import SiteStorage from '@generated/site-storage';
|
|
|
|
export type StorageType = (typeof SiteStorage)['type'] | 'none';
|
|
|
|
const DefaultStorageType: StorageType = SiteStorage.type;
|
|
|
|
function applyNamespace(storageKey: string): string {
|
|
return `${storageKey}${SiteStorage.namespace}`;
|
|
}
|
|
|
|
// window.addEventListener('storage') only works for different windows...
|
|
// so for current window we have to dispatch the event manually
|
|
// Now we can listen for both cross-window / current-window storage changes!
|
|
// see https://stackoverflow.com/a/71177640/82609
|
|
// see https://stackoverflow.com/questions/26974084/listen-for-changes-with-localstorage-on-the-same-window
|
|
function dispatchChangeEvent({
|
|
key,
|
|
oldValue,
|
|
newValue,
|
|
storage,
|
|
}: {
|
|
key: string;
|
|
oldValue: string | null;
|
|
newValue: string | null;
|
|
storage: Storage;
|
|
}) {
|
|
// If we set multiple times the same storage value, events should not be fired
|
|
// The native events behave this way, so our manual event dispatch should
|
|
// rather behave exactly the same. Not doing so might create infinite loops.
|
|
// See https://github.com/facebook/docusaurus/issues/8594
|
|
if (oldValue === newValue) {
|
|
return;
|
|
}
|
|
const event = document.createEvent('StorageEvent');
|
|
event.initStorageEvent(
|
|
'storage',
|
|
false,
|
|
false,
|
|
key,
|
|
oldValue,
|
|
newValue,
|
|
window.location.href,
|
|
storage,
|
|
);
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Will return `null` if browser storage is unavailable (like running Docusaurus
|
|
* in an iframe). This should NOT be called in SSR.
|
|
*
|
|
* @see https://github.com/facebook/docusaurus/pull/4501
|
|
*/
|
|
function getBrowserStorage(
|
|
storageType: StorageType = DefaultStorageType,
|
|
): Storage | null {
|
|
if (typeof window === 'undefined') {
|
|
throw new Error(
|
|
'Browser storage is not available on Node.js/Docusaurus SSR process.',
|
|
);
|
|
}
|
|
if (storageType === 'none') {
|
|
return null;
|
|
}
|
|
try {
|
|
return window[storageType];
|
|
} catch (err) {
|
|
logOnceBrowserStorageNotAvailableWarning(err as Error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
let hasLoggedBrowserStorageNotAvailableWarning = false;
|
|
/**
|
|
* Poor man's memoization to avoid logging multiple times the same warning.
|
|
* Sometimes, `localStorage`/`sessionStorage` is unavailable due to browser
|
|
* policies.
|
|
*/
|
|
function logOnceBrowserStorageNotAvailableWarning(error: Error) {
|
|
if (!hasLoggedBrowserStorageNotAvailableWarning) {
|
|
console.warn(
|
|
`Docusaurus browser storage is not available.
|
|
Possible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.`,
|
|
error,
|
|
);
|
|
hasLoggedBrowserStorageNotAvailableWarning = true;
|
|
}
|
|
}
|
|
|
|
// Convenient storage interface for a single storage key
|
|
export type StorageSlot = {
|
|
get: () => string | null;
|
|
set: (value: string) => void;
|
|
del: () => void;
|
|
listen: (onChange: (event: StorageEvent) => void) => () => void;
|
|
};
|
|
|
|
const NoopStorageSlot: StorageSlot = {
|
|
get: () => null,
|
|
set: () => {},
|
|
del: () => {},
|
|
listen: () => () => {},
|
|
};
|
|
|
|
// Fail-fast, as storage APIs should not be used during the SSR process
|
|
function createServerStorageSlot(key: string): StorageSlot {
|
|
function throwError(): never {
|
|
throw new Error(`Illegal storage API usage for storage key "${key}".
|
|
Docusaurus storage APIs are not supposed to be called on the server-rendering process.
|
|
Please only call storage APIs in effects and event handlers.`);
|
|
}
|
|
|
|
return {
|
|
get: throwError,
|
|
set: throwError,
|
|
del: throwError,
|
|
listen: throwError,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates an interface to work on a particular key in the storage model.
|
|
* Note that this function only initializes the interface, but doesn't allocate
|
|
* anything by itself (i.e. no side-effects).
|
|
*
|
|
* The API is fail-safe, since usage of browser storage should be considered
|
|
* unreliable. Local storage might simply be unavailable (iframe + browser
|
|
* security) or operations might fail individually. Please assume that using
|
|
* this API can be a no-op. See also https://github.com/facebook/docusaurus/issues/6036
|
|
*/
|
|
export function createStorageSlot(
|
|
keyInput: string,
|
|
options?: {persistence?: StorageType},
|
|
): StorageSlot {
|
|
const key = applyNamespace(keyInput);
|
|
if (typeof window === 'undefined') {
|
|
return createServerStorageSlot(key);
|
|
}
|
|
const storage = getBrowserStorage(options?.persistence);
|
|
if (storage === null) {
|
|
return NoopStorageSlot;
|
|
}
|
|
return {
|
|
get: () => {
|
|
try {
|
|
return storage.getItem(key);
|
|
} catch (err) {
|
|
console.error(`Docusaurus storage error, can't get key=${key}`, err);
|
|
return null;
|
|
}
|
|
},
|
|
set: (newValue) => {
|
|
try {
|
|
const oldValue = storage.getItem(key);
|
|
storage.setItem(key, newValue);
|
|
dispatchChangeEvent({
|
|
key,
|
|
oldValue,
|
|
newValue,
|
|
storage,
|
|
});
|
|
} catch (err) {
|
|
console.error(
|
|
`Docusaurus storage error, can't set ${key}=${newValue}`,
|
|
err,
|
|
);
|
|
}
|
|
},
|
|
del: () => {
|
|
try {
|
|
const oldValue = storage.getItem(key);
|
|
storage.removeItem(key);
|
|
dispatchChangeEvent({key, oldValue, newValue: null, storage});
|
|
} catch (err) {
|
|
console.error(`Docusaurus storage error, can't delete key=${key}`, err);
|
|
}
|
|
},
|
|
listen: (onChange) => {
|
|
try {
|
|
const listener = (event: StorageEvent) => {
|
|
if (event.storageArea === storage && event.key === key) {
|
|
onChange(event);
|
|
}
|
|
};
|
|
window.addEventListener('storage', listener);
|
|
return () => window.removeEventListener('storage', listener);
|
|
} catch (err) {
|
|
console.error(
|
|
`Docusaurus storage error, can't listen for changes of key=${key}`,
|
|
err,
|
|
);
|
|
return () => {};
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
export function useStorageSlot(
|
|
key: string | null,
|
|
options?: {persistence?: StorageType},
|
|
): [string | null, StorageSlot] {
|
|
// Not ideal but good enough: assumes storage slot config is constant
|
|
const storageSlot = useRef(() => {
|
|
if (key === null) {
|
|
return NoopStorageSlot;
|
|
}
|
|
return createStorageSlot(key, options);
|
|
}).current();
|
|
|
|
const listen: StorageSlot['listen'] = useCallback(
|
|
(onChange) => {
|
|
// Do not try to add a listener during SSR
|
|
if (typeof window === 'undefined') {
|
|
return () => {};
|
|
}
|
|
return storageSlot.listen(onChange);
|
|
},
|
|
[storageSlot],
|
|
);
|
|
|
|
const currentValue = useSyncExternalStore(
|
|
listen,
|
|
() => {
|
|
// react-test-renderer (deprecated) never call getServerSnapshot() :/
|
|
if (process.env.NODE_ENV === 'test') {
|
|
return null;
|
|
}
|
|
return storageSlot.get();
|
|
},
|
|
() => null,
|
|
);
|
|
|
|
return [currentValue, storageSlot];
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all the keys currently stored in browser storage,
|
|
* or an empty list if browser storage can't be accessed.
|
|
*/
|
|
export function listStorageKeys(
|
|
storageType: StorageType = DefaultStorageType,
|
|
): string[] {
|
|
const browserStorage = getBrowserStorage(storageType);
|
|
if (!browserStorage) {
|
|
return [];
|
|
}
|
|
|
|
const keys: string[] = [];
|
|
for (let i = 0; i < browserStorage.length; i += 1) {
|
|
const key = browserStorage.key(i);
|
|
if (key !== null) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
return keys;
|
|
}
|