mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-21 18:47:50 +02:00
feat(v2): @docusaurus/theme-common (#3775)
* create base @docusaurus/theme-common package + fix Webpack client export aliases issue shadowing other theme-common package * Move theme-classic/src/utils code to new @docusaurus/theme-common package * add prettierignore * fix bad test location for getDocusaurusAliases()
This commit is contained in:
parent
5872bbc735
commit
abcd8cefd6
39 changed files with 176 additions and 32 deletions
19
packages/docusaurus-theme-common/src/index.ts
Normal file
19
packages/docusaurus-theme-common/src/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {useThemeConfig, ThemeConfig} from './utils/useThemeConfig';
|
||||
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
export {isDocsPluginEnabled} from './utils/docsUtils';
|
||||
|
||||
export {isSamePath} from './utils/pathUtils';
|
||||
|
||||
export {
|
||||
useDocsPreferredVersion,
|
||||
useDocsPreferredVersionByPluginId,
|
||||
} from './utils/docsPreferredVersion/useDocsPreferredVersion';
|
||||
|
||||
export {DocsPreferredVersionContextProvider} from './utils/docsPreferredVersion/DocsPreferredVersionProvider';
|
13
packages/docusaurus-theme-common/src/types.d.ts
vendored
Normal file
13
packages/docusaurus-theme-common/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable import/no-duplicates */
|
||||
/* eslint-disable spaced-comment */
|
||||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
/// <reference types="@docusaurus/plugin-content-blog" />
|
||||
/// <reference types="@docusaurus/plugin-content-docs" />
|
||||
/// <reference types="@docusaurus/plugin-content-pages" />
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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 {isSamePath} from '../pathUtils';
|
||||
|
||||
describe('isSamePath', () => {
|
||||
test('should be true for compared path without trailing slash', () => {
|
||||
expect(isSamePath('/docs', '/docs')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should be true for compared path with trailing slash', () => {
|
||||
expect(isSamePath('/docs', '/docs/')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should be false for compared path with double trailing slash', () => {
|
||||
expect(isSamePath('/docs', '/docs//')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should be true for twice undefined/null', () => {
|
||||
expect(isSamePath(undefined, undefined)).toBeTruthy();
|
||||
expect(isSamePath(undefined, undefined)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should be false when one undefined', () => {
|
||||
expect(isSamePath('/docs', undefined)).toBeFalsy();
|
||||
expect(isSamePath(undefined, '/docs')).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* 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 React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {useThemeConfig, DocsVersionPersistence} from '../useThemeConfig';
|
||||
import {isDocsPluginEnabled} from '../docsUtils';
|
||||
|
||||
import {useAllDocsData} from '@theme/hooks/useDocs';
|
||||
|
||||
import DocsPreferredVersionStorage from './DocsPreferredVersionStorage';
|
||||
|
||||
type DocsPreferredVersionName = string | null;
|
||||
|
||||
// State for a single docs plugin instance
|
||||
type DocsPreferredVersionPluginState = {
|
||||
preferredVersionName: DocsPreferredVersionName;
|
||||
};
|
||||
|
||||
// We need to store in state/storage globally
|
||||
// one preferred version per docs plugin instance
|
||||
// pluginId => pluginState
|
||||
type DocsPreferredVersionState = Record<
|
||||
string,
|
||||
DocsPreferredVersionPluginState
|
||||
>;
|
||||
|
||||
// Initial state is always null as we can't read localstorage from node SSR
|
||||
function getInitialState(pluginIds: string[]): DocsPreferredVersionState {
|
||||
const initialState: DocsPreferredVersionState = {};
|
||||
pluginIds.forEach((pluginId) => {
|
||||
initialState[pluginId] = {
|
||||
preferredVersionName: null,
|
||||
};
|
||||
});
|
||||
return initialState;
|
||||
}
|
||||
|
||||
// Read storage for all docs plugins
|
||||
// Assign to each doc plugin a preferred version (if found)
|
||||
function readStorageState({
|
||||
pluginIds,
|
||||
versionPersistence,
|
||||
allDocsData,
|
||||
}: {
|
||||
pluginIds: string[];
|
||||
versionPersistence: DocsVersionPersistence;
|
||||
allDocsData: any; // TODO find a way to type it :(
|
||||
}): DocsPreferredVersionState {
|
||||
// The storage value we read might be stale,
|
||||
// and belong to a version that does not exist in the site anymore
|
||||
// In such case, we remove the storage value to avoid downstream errors
|
||||
function restorePluginState(
|
||||
pluginId: string,
|
||||
): DocsPreferredVersionPluginState {
|
||||
const preferredVersionNameUnsafe = DocsPreferredVersionStorage.read(
|
||||
pluginId,
|
||||
versionPersistence,
|
||||
);
|
||||
const pluginData = allDocsData[pluginId];
|
||||
const versionExists = pluginData.versions.some(
|
||||
(version: any) => version.name === preferredVersionNameUnsafe,
|
||||
);
|
||||
if (versionExists) {
|
||||
return {preferredVersionName: preferredVersionNameUnsafe};
|
||||
} else {
|
||||
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
|
||||
return {preferredVersionName: null};
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: DocsPreferredVersionState = {};
|
||||
pluginIds.forEach((pluginId) => {
|
||||
initialState[pluginId] = restorePluginState(pluginId);
|
||||
});
|
||||
return initialState;
|
||||
}
|
||||
|
||||
function useVersionPersistence(): DocsVersionPersistence {
|
||||
return useThemeConfig().docs.versionPersistence;
|
||||
}
|
||||
|
||||
// Value that will be accessible through context: [state,api]
|
||||
function useContextValue() {
|
||||
const allDocsData = useAllDocsData();
|
||||
const versionPersistence = useVersionPersistence();
|
||||
const pluginIds = useMemo(() => Object.keys(allDocsData), [allDocsData]);
|
||||
|
||||
// Initial state is empty, as we can't read browser storage in node/SSR
|
||||
const [state, setState] = useState(() => getInitialState(pluginIds));
|
||||
|
||||
// On mount, we set the state read from browser storage
|
||||
useEffect(() => {
|
||||
setState(readStorageState({allDocsData, versionPersistence, pluginIds}));
|
||||
}, [allDocsData, versionPersistence, pluginIds]);
|
||||
|
||||
// The API that we expose to consumer hooks (memo for constant object)
|
||||
const api = useMemo(() => {
|
||||
function savePreferredVersion(pluginId: string, versionName: string) {
|
||||
DocsPreferredVersionStorage.save(
|
||||
pluginId,
|
||||
versionPersistence,
|
||||
versionName,
|
||||
);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
[pluginId]: {preferredVersionName: versionName},
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
savePreferredVersion,
|
||||
};
|
||||
}, [setState]);
|
||||
|
||||
return [state, api] as const;
|
||||
}
|
||||
|
||||
type DocsPreferredVersionContextValue = ReturnType<typeof useContextValue>;
|
||||
|
||||
const Context = createContext<DocsPreferredVersionContextValue | null>(null);
|
||||
|
||||
export function DocsPreferredVersionContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (isDocsPluginEnabled) {
|
||||
return (
|
||||
<DocsPreferredVersionContextProviderUnsafe>
|
||||
{children}
|
||||
</DocsPreferredVersionContextProviderUnsafe>
|
||||
);
|
||||
} else {
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
function DocsPreferredVersionContextProviderUnsafe({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const contextValue = useContextValue();
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue {
|
||||
const value = useContext(Context);
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
"Can't find docs preferred context, maybe you forgot to use the DocsPreferredVersionContextProvider ?",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* 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 {DocsVersionPersistence} from '../useThemeConfig';
|
||||
|
||||
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
|
||||
|
||||
const DocsPreferredVersionStorage = {
|
||||
save: (
|
||||
pluginId: string,
|
||||
persistence: DocsVersionPersistence,
|
||||
versionName: string,
|
||||
): void => {
|
||||
if (persistence === 'none') {
|
||||
// noop
|
||||
} else {
|
||||
window.localStorage.setItem(storageKey(pluginId), versionName);
|
||||
}
|
||||
},
|
||||
|
||||
read: (
|
||||
pluginId: string,
|
||||
persistence: DocsVersionPersistence,
|
||||
): string | null => {
|
||||
if (persistence === 'none') {
|
||||
return null;
|
||||
} else {
|
||||
return window.localStorage.getItem(storageKey(pluginId));
|
||||
}
|
||||
},
|
||||
|
||||
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
|
||||
if (persistence === 'none') {
|
||||
// noop
|
||||
} else {
|
||||
window.localStorage.removeItem(storageKey(pluginId));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default DocsPreferredVersionStorage;
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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} from 'react';
|
||||
import {useDocsPreferredVersionContext} from './DocsPreferredVersionProvider';
|
||||
import {useAllDocsData, useDocsData} from '@theme/hooks/useDocs';
|
||||
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';
|
||||
|
||||
// TODO improve typing
|
||||
|
||||
// Note, the preferredVersion attribute will always be null before mount
|
||||
export function useDocsPreferredVersion(
|
||||
pluginId: string | undefined = DEFAULT_PLUGIN_ID,
|
||||
) {
|
||||
const docsData = useDocsData(pluginId);
|
||||
const [state, api] = useDocsPreferredVersionContext();
|
||||
|
||||
const {preferredVersionName} = state[pluginId];
|
||||
|
||||
const preferredVersion = preferredVersionName
|
||||
? docsData.versions.find(
|
||||
(version: any) => version.name === preferredVersionName,
|
||||
)
|
||||
: null;
|
||||
|
||||
const savePreferredVersionName = useCallback(
|
||||
(versionName: string) => {
|
||||
api.savePreferredVersion(pluginId, versionName);
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
return {preferredVersion, savePreferredVersionName} as const;
|
||||
}
|
||||
|
||||
export function useDocsPreferredVersionByPluginId() {
|
||||
const allDocsData = useAllDocsData();
|
||||
const [state] = useDocsPreferredVersionContext();
|
||||
|
||||
function getPluginIdPreferredVersion(pluginId: string) {
|
||||
const docsData = allDocsData[pluginId];
|
||||
const {preferredVersionName} = state[pluginId];
|
||||
|
||||
return preferredVersionName
|
||||
? docsData.versions.find(
|
||||
(version: any) => version.name === preferredVersionName,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
const pluginIds = Object.keys(allDocsData);
|
||||
|
||||
const result: Record<
|
||||
string,
|
||||
any // TODO find a way to type this properly!
|
||||
> = {};
|
||||
pluginIds.forEach((pluginId) => {
|
||||
result[pluginId] = getPluginIdPreferredVersion(pluginId);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
11
packages/docusaurus-theme-common/src/utils/docsUtils.ts
Normal file
11
packages/docusaurus-theme-common/src/utils/docsUtils.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* 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 {useAllDocsData} from '@theme/hooks/useDocs';
|
||||
|
||||
// TODO not ideal, see also "useDocs"
|
||||
export const isDocsPluginEnabled: boolean = !!useAllDocsData;
|
17
packages/docusaurus-theme-common/src/utils/pathUtils.ts
Normal file
17
packages/docusaurus-theme-common/src/utils/pathUtils.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Compare the 2 paths, ignoring trailing /
|
||||
export const isSamePath = (
|
||||
path1: string | undefined,
|
||||
path2: string | undefined,
|
||||
) => {
|
||||
const normalize = (pathname: string | undefined) => {
|
||||
return !pathname || pathname?.endsWith('/') ? pathname : `${pathname}/`;
|
||||
};
|
||||
return normalize(path1) === normalize(path2);
|
||||
};
|
12
packages/docusaurus-theme-common/src/utils/searchUtils.ts
Normal file
12
packages/docusaurus-theme-common/src/utils/searchUtils.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const DEFAULT_SEARCH_TAG = 'default';
|
||||
|
||||
export function docVersionSearchTag(pluginId: string, versionName: string) {
|
||||
return `docs-${pluginId}-${versionName}`;
|
||||
}
|
31
packages/docusaurus-theme-common/src/utils/useThemeConfig.ts
Normal file
31
packages/docusaurus-theme-common/src/utils/useThemeConfig.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* 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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
export type DocsVersionPersistence = 'localStorage' | 'none';
|
||||
|
||||
export type ThemeConfig = {
|
||||
docs: {
|
||||
versionPersistence: DocsVersionPersistence;
|
||||
};
|
||||
|
||||
// TODO we should complete this theme config type over time
|
||||
// and share it across all themes
|
||||
// and use it in the Joi validation schema?
|
||||
|
||||
// TODO temporary types
|
||||
navbar: any;
|
||||
colorMode: any;
|
||||
announcementBar: any;
|
||||
prism: any;
|
||||
footer: any;
|
||||
hideableSidebar: any;
|
||||
};
|
||||
|
||||
export function useThemeConfig(): ThemeConfig {
|
||||
return useDocusaurusContext().siteConfig.themeConfig as ThemeConfig;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue