refactor(theme-search-algolia): migrate package to TS (#5935)

This commit is contained in:
Armano 2021-11-16 20:35:09 +01:00 committed by GitHub
parent 284cdabb0a
commit 425144afc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 301 additions and 91 deletions

View file

@ -14,6 +14,7 @@ packages/docusaurus/lib/
packages/docusaurus-*/lib/* packages/docusaurus-*/lib/*
packages/docusaurus-*/lib-next/ packages/docusaurus-*/lib-next/
packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js
packages/docusaurus-theme-search-algolia/copyUntypedFiles.js
packages/create-docusaurus/lib/* packages/create-docusaurus/lib/*
packages/create-docusaurus/templates/facebook/.eslintrc.js packages/create-docusaurus/templates/facebook/.eslintrc.js

View file

@ -15,6 +15,6 @@ const srcDir = path.resolve(__dirname, 'src');
const libDir = path.resolve(__dirname, 'lib'); const libDir = path.resolve(__dirname, 'lib');
fs.copySync(srcDir, libDir, { fs.copySync(srcDir, libDir, {
filter(filepath) { filter(filepath) {
return !/__tests__/.test(filepath) && !/\.ts$/.test(filepath); return !/__tests__/.test(filepath) && !/\.tsx?$/.test(filepath);
}, },
}); });

View file

@ -15,7 +15,7 @@ const CodeDirPaths = [
path.join(__dirname, 'lib-next'), path.join(__dirname, 'lib-next'),
// TODO other themes should rather define their own translations in the future? // TODO other themes should rather define their own translations in the future?
path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'), path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'),
path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'lib', 'theme'),
path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'),
path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'), path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'),
]; ];

View file

@ -22,6 +22,8 @@ export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
export {useAlternatePageUtils} from './utils/useAlternatePageUtils'; export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
export {useContextualSearchFilters} from './utils/useContextualSearchFilters';
export { export {
parseCodeBlockTitle, parseCodeBlockTitle,
parseLanguage, parseLanguage,

View file

@ -5,21 +5,18 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {useAllDocsData, useActivePluginAndVersion} from '@theme/hooks/useDocs'; import {useAllDocsData, useActivePluginAndVersion} from '@theme/hooks/useDocs';
import { import {useDocsPreferredVersionByPluginId} from './docsPreferredVersion/useDocsPreferredVersion';
useDocsPreferredVersionByPluginId, import {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './searchUtils';
DEFAULT_SEARCH_TAG,
docVersionSearchTag,
} from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
type ContextualSearchFilters = { export type useContextualSearchFiltersReturns = {
locale: string; locale: string;
tags: string[]; tags: string[];
}; };
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch // We may want to support multiple search engines, don't couple that to Algolia/DocSearch
// Maybe users will want to use its own search engine solution // Maybe users will want to use its own search engine solution
export default function useContextualSearchFilters(): ContextualSearchFilters { export function useContextualSearchFilters(): useContextualSearchFiltersReturns {
const {i18n} = useDocusaurusContext(); const {i18n} = useDocusaurusContext();
const allDocsData = useAllDocsData(); const allDocsData = useAllDocsData();
const activePluginAndVersion = useActivePluginAndVersion(); const activePluginAndVersion = useActivePluginAndVersion();

View file

@ -0,0 +1,20 @@
/**
* 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.
*/
const path = require('path');
const fs = require('fs-extra');
/**
* Copy all untyped and static assets files to lib.
*/
const srcDir = path.resolve(__dirname, 'src');
const libDir = path.resolve(__dirname, 'lib');
fs.copySync(srcDir, libDir, {
filter(filepath) {
return !/__tests__/.test(filepath) && !/\.tsx?$/.test(filepath);
},
});

View file

@ -2,7 +2,8 @@
"name": "@docusaurus/theme-search-algolia", "name": "@docusaurus/theme-search-algolia",
"version": "2.0.0-beta.9", "version": "2.0.0-beta.9",
"description": "Algolia search component for Docusaurus.", "description": "Algolia search component for Docusaurus.",
"main": "src/index.js", "main": "lib/index.js",
"types": "src/theme-search-algolia.d.ts",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@ -12,6 +13,12 @@
"directory": "packages/docusaurus-theme-search-algolia" "directory": "packages/docusaurus-theme-search-algolia"
}, },
"license": "MIT", "license": "MIT",
"scripts": {
"build": "yarn build:server && yarn build:browser && yarn build:copy",
"build:server": "tsc --project tsconfig.server.json",
"build:browser": "tsc --project tsconfig.browser.json",
"build:copy": "node copyUntypedFiles.js"
},
"dependencies": { "dependencies": {
"@docsearch/react": "^3.0.0-alpha.39", "@docsearch/react": "^3.0.0-alpha.39",
"@docusaurus/core": "2.0.0-beta.9", "@docusaurus/core": "2.0.0-beta.9",
@ -24,6 +31,10 @@
"eta": "^1.12.3", "eta": "^1.12.3",
"lodash": "^4.17.20" "lodash": "^4.17.20"
}, },
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.9",
"fs-extra": "^10.0.0"
},
"peerDependencies": { "peerDependencies": {
"react": "^16.8.4 || ^17.0.0", "react": "^16.8.4 || ^17.0.0",
"react-dom": "^16.8.4 || ^17.0.0" "react-dom": "^16.8.4 || ^17.0.0"

View file

@ -5,26 +5,33 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
const path = require('path'); import path from 'path';
const fs = require('fs'); import fs from 'fs';
const eta = require('eta'); import {defaultConfig, compile} from 'eta';
const {normalizeUrl, getSwizzledComponent} = require('@docusaurus/utils'); import {normalizeUrl, getSwizzledComponent} from '@docusaurus/utils';
const openSearchTemplate = require('./templates/opensearch'); import openSearchTemplate from './templates/opensearch';
const {validateThemeConfig} = require('./validateThemeConfig'); import {memoize} from 'lodash';
const {memoize} = require('lodash');
import type {DocusaurusContext, Plugin} from '@docusaurus/types';
const getCompiledOpenSearchTemplate = memoize(() => { const getCompiledOpenSearchTemplate = memoize(() => {
return eta.compile(openSearchTemplate.trim()); return compile(openSearchTemplate.trim());
}); });
function renderOpenSearchTemplate(data) { function renderOpenSearchTemplate(data: {
title: string;
url: string;
favicon: string | null;
}) {
const compiled = getCompiledOpenSearchTemplate(); const compiled = getCompiledOpenSearchTemplate();
return compiled(data, eta.defaultConfig); return compiled(data, defaultConfig);
} }
const OPEN_SEARCH_FILENAME = 'opensearch.xml'; const OPEN_SEARCH_FILENAME = 'opensearch.xml';
function theme(context) { export default function theme(
context: DocusaurusContext & {baseUrl: string},
): Plugin<void> {
const { const {
baseUrl, baseUrl,
siteConfig: {title, url, favicon}, siteConfig: {title, url, favicon},
@ -37,12 +44,16 @@ function theme(context) {
return { return {
name: 'docusaurus-theme-search-algolia', name: 'docusaurus-theme-search-algolia',
getPathsToWatch() {
return [pagePath];
},
getThemePath() { getThemePath() {
return path.resolve(__dirname, './theme'); return path.resolve(__dirname, './theme');
}, },
getPathsToWatch() { getTypeScriptThemePath() {
return [pagePath]; return path.resolve(__dirname, '..', 'src', 'theme');
}, },
async contentLoaded({actions: {addRoute}}) { async contentLoaded({actions: {addRoute}}) {
@ -87,6 +98,4 @@ function theme(context) {
}; };
} }
module.exports = theme; export {validateThemeConfig} from './validateThemeConfig';
theme.validateThemeConfig = validateThemeConfig;

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
module.exports = ` export default `
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/"> xmlns:moz="http://www.mozilla.org/2006/browser/search/">

View file

@ -0,0 +1,49 @@
/**
* 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.
*/
declare module '@docusaurus/theme-search-algolia' {
export type Options = never;
}
declare module '@theme/hooks/useSearchQuery' {
export interface SearchQuery {
searchQuery: string;
setSearchQuery(newSearchQuery: string): void;
generateSearchPageLink(targetSearchQuery: string): string;
}
export default function useSearchQuery(): SearchQuery;
}
declare module '@theme/hooks/useAlgoliaContextualFacetFilters' {
export type useAlgoliaContextualFacetFiltersReturns = [string, string[]];
export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns;
}
declare module '@theme/SearchPage' {
const SearchPage: () => JSX.Element;
export default SearchPage;
}
declare module '@theme/SearchMetadata' {
export type SearchMetadataProps = {
readonly locale?: string;
readonly version?: string;
readonly tag?: string;
};
const SearchMetadata: (props: SearchMetadataProps) => JSX.Element;
export default SearchMetadata;
}
declare module '@theme/SearchBar' {
const SearchBar: () => JSX.Element;
export default SearchBar;
}

View file

@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, {useState, useRef, useCallback, useMemo} from 'react'; import React, {useState, useRef, useCallback, useMemo} from 'react';
import {createPortal} from 'react-dom'; import {createPortal} from 'react-dom';
@ -19,13 +20,42 @@ import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualF
import {translate} from '@docusaurus/Translate'; import {translate} from '@docusaurus/Translate';
import styles from './styles.module.css'; import styles from './styles.module.css';
let DocSearchModal = null; import type {
DocSearchModal as DocSearchModalType,
DocSearchModalProps,
} from '@docsearch/react';
import type {
InternalDocSearchHit,
StoredDocSearchHit,
} from '@docsearch/react/dist/esm/types';
import type {AutocompleteState} from '@algolia/autocomplete-core';
function Hit({hit, children}) { type DocSearchProps = Omit<
DocSearchModalProps,
'onClose' | 'initialScrollY'
> & {
contextualSearch?: string;
externalUrlRegex?: string;
};
let DocSearchModal: typeof DocSearchModalType | null = null;
function Hit({
hit,
children,
}: {
hit: InternalDocSearchHit | StoredDocSearchHit;
children: React.ReactNode;
}) {
return <Link to={hit.url}>{children}</Link>; return <Link to={hit.url}>{children}</Link>;
} }
function ResultsFooter({state, onClose}) { type ResultsFooterProps = {
state: AutocompleteState<InternalDocSearchHit>;
onClose: () => void;
};
function ResultsFooter({state, onClose}: ResultsFooterProps) {
const {generateSearchPageLink} = useSearchQuery(); const {generateSearchPageLink} = useSearchQuery();
return ( return (
@ -35,7 +65,11 @@ function ResultsFooter({state, onClose}) {
); );
} }
function DocSearch({contextualSearch, externalUrlRegex, ...props}) { function DocSearch({
contextualSearch,
externalUrlRegex,
...props
}: DocSearchProps) {
const {siteMetadata} = useDocusaurusContext(); const {siteMetadata} = useDocusaurusContext();
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters(); const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
@ -56,10 +90,12 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
const {withBaseUrl} = useBaseUrlUtils(); const {withBaseUrl} = useBaseUrlUtils();
const history = useHistory(); const history = useHistory();
const searchContainer = useRef(null); const searchContainer = useRef<HTMLDivElement | null>(null);
const searchButtonRef = useRef(null); const searchButtonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [initialQuery, setInitialQuery] = useState(null); const [initialQuery, setInitialQuery] = useState<string | undefined>(
undefined,
);
const importDocSearchModalIfNeeded = useCallback(() => { const importDocSearchModalIfNeeded = useCallback(() => {
if (DocSearchModal) { if (DocSearchModal) {
@ -67,7 +103,9 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
} }
return Promise.all([ return Promise.all([
// @ts-ignore
import('@docsearch/react/modal'), import('@docsearch/react/modal'),
// @ts-ignore
import('@docsearch/react/style'), import('@docsearch/react/style'),
import('./styles.css'), import('./styles.css'),
]).then(([{DocSearchModal: Modal}]) => { ]).then(([{DocSearchModal: Modal}]) => {
@ -88,7 +126,7 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
const onClose = useCallback(() => { const onClose = useCallback(() => {
setIsOpen(false); setIsOpen(false);
searchContainer.current.remove(); searchContainer.current?.remove();
}, [setIsOpen]); }, [setIsOpen]);
const onInput = useCallback( const onInput = useCallback(
@ -102,35 +140,38 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
); );
const navigator = useRef({ const navigator = useRef({
navigate({itemUrl}) { navigate({itemUrl}: {itemUrl?: string}) {
// Algolia results could contain URL's from other domains which cannot // Algolia results could contain URL's from other domains which cannot
// be served through history and should navigate with window.location // be served through history and should navigate with window.location
if (isRegexpStringMatch(externalUrlRegex, itemUrl)) { if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
window.location.href = itemUrl; window.location.href = itemUrl!;
} else { } else {
history.push(itemUrl); history.push(itemUrl!);
} }
}, },
}).current; }).current;
const transformItems = useRef((items) => { const transformItems = useRef<DocSearchModalProps['transformItems']>(
return items.map((item) => { (items) => {
// If Algolia contains a external domain, we should navigate without relative URL return items.map((item) => {
if (isRegexpStringMatch(externalUrlRegex, item.url)) { // If Algolia contains a external domain, we should navigate without relative URL
return item; if (isRegexpStringMatch(externalUrlRegex, item.url)) {
} return item;
}
// We transform the absolute URL into a relative URL. // We transform the absolute URL into a relative URL.
const url = new URL(item.url); const url = new URL(item.url);
return { return {
...item, ...item,
url: withBaseUrl(`${url.pathname}${url.hash}`), url: withBaseUrl(`${url.pathname}${url.hash}`),
}; };
}); });
}).current; },
).current;
const resultsFooterComponent = useMemo( const resultsFooterComponent = useMemo(
() => (footerProps) => <ResultsFooter {...footerProps} onClose={onClose} />, () => (footerProps: ResultsFooterProps) =>
<ResultsFooter {...footerProps} onClose={onClose} />,
[onClose], [onClose],
); );
@ -188,6 +229,8 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
</div> </div>
{isOpen && {isOpen &&
DocSearchModal &&
searchContainer.current &&
createPortal( createPortal(
<DocSearchModal <DocSearchModal
onClose={onClose} onClose={onClose}
@ -209,6 +252,7 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
function SearchBar() { function SearchBar() {
const {siteConfig} = useDocusaurusContext(); const {siteConfig} = useDocusaurusContext();
// @ts-ignore
return <DocSearch {...siteConfig.themeConfig.algolia} />; return <DocSearch {...siteConfig.themeConfig.algolia} />;
} }

View file

@ -8,9 +8,14 @@
import React from 'react'; import React from 'react';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import type {SearchMetadataProps} from '@theme/SearchMetadata';
// Override default/agnostic SearchMetas to use Algolia-specific metadata // Override default/agnostic SearchMetas to use Algolia-specific metadata
export default function AlgoliaSearchMetadata({locale, version, tag}) { function SearchMetadata({
locale,
version,
tag,
}: SearchMetadataProps): JSX.Element {
// Seems safe to consider here the locale is the language, // Seems safe to consider here the locale is the language,
// as the existing docsearch:language filter is afaik a regular string-based filter // as the existing docsearch:language filter is afaik a regular string-based filter
const language = locale; const language = locale;
@ -23,3 +28,5 @@ export default function AlgoliaSearchMetadata({locale, version, tag}) {
</Head> </Head>
); );
} }
export default SearchMetadata;

View file

@ -6,6 +6,7 @@
*/ */
/* eslint-disable jsx-a11y/no-autofocus */ /* eslint-disable jsx-a11y/no-autofocus */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, {useEffect, useState, useReducer, useRef} from 'react'; import React, {useEffect, useState, useReducer, useRef} from 'react';
@ -32,7 +33,7 @@ import styles from './styles.module.css';
// Very simple pluralization: probably good enough for now // Very simple pluralization: probably good enough for now
function useDocumentsFoundPlural() { function useDocumentsFoundPlural() {
const {selectMessage} = usePluralForm(); const {selectMessage} = usePluralForm();
return (count) => return (count: number) =>
selectMessage( selectMessage(
count, count,
translate( translate(
@ -52,14 +53,19 @@ function useDocsSearchVersionsHelpers() {
// State of the version select menus / algolia facet filters // State of the version select menus / algolia facet filters
// docsPluginId -> versionName map // docsPluginId -> versionName map
const [searchVersions, setSearchVersions] = useState(() => { const [searchVersions, setSearchVersions] = useState<Record<string, string>>(
return Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => { () => {
return {...acc, [pluginId]: pluginData.versions[0].name}; return Object.entries(allDocsData).reduce(
}, {}); (acc, [pluginId, pluginData]) => {
}); return {...acc, [pluginId]: pluginData.versions[0].name};
},
{},
);
},
);
// Set the value of a single select menu // Set the value of a single select menu
const setSearchVersion = (pluginId, searchVersion) => const setSearchVersion = (pluginId: string, searchVersion: string) =>
setSearchVersions((s) => ({...s, [pluginId]: searchVersion})); setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
const versioningEnabled = Object.values(allDocsData).some( const versioningEnabled = Object.values(allDocsData).some(
@ -75,7 +81,11 @@ function useDocsSearchVersionsHelpers() {
} }
// We want to display one select per versioned docs plugin instance // We want to display one select per versioned docs plugin instance
const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => { const SearchVersionSelectList = ({
docsSearchVersionsHelpers,
}: {
docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>;
}) => {
const versionedPluginEntries = Object.entries( const versionedPluginEntries = Object.entries(
docsSearchVersionsHelpers.allDocsData, docsSearchVersionsHelpers.allDocsData,
) )
@ -118,10 +128,32 @@ const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
); );
}; };
function SearchPage() { type ResultDispatcherState = {
items: {
title: string;
url: string;
summary: string;
breadcrumbs: string[];
}[];
query: string | null;
totalResults: number | null;
totalPages: number | null;
lastPage: number | null;
hasMore: boolean | null;
loading: boolean | null;
};
type ResultDispatcher =
| {type: 'reset'; value?: undefined}
| {type: 'loading'; value?: undefined}
| {type: 'update'; value: ResultDispatcherState}
| {type: 'advance'; value?: undefined};
function SearchPage(): JSX.Element {
const { const {
siteConfig: { siteConfig: {
themeConfig: { themeConfig: {
// @ts-ignore
algolia: {appId, apiKey, indexName, externalUrlRegex}, algolia: {appId, apiKey, indexName, externalUrlRegex},
}, },
}, },
@ -131,7 +163,7 @@ function SearchPage() {
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
const {searchQuery, setSearchQuery} = useSearchQuery(); const {searchQuery, setSearchQuery} = useSearchQuery();
const initialSearchResultState = { const initialSearchResultState: ResultDispatcherState = {
items: [], items: [],
query: null, query: null,
totalResults: null, totalResults: null,
@ -141,8 +173,8 @@ function SearchPage() {
loading: null, loading: null,
}; };
const [searchResultState, searchResultStateDispatcher] = useReducer( const [searchResultState, searchResultStateDispatcher] = useReducer(
(prevState, {type, value: state}) => { (prevState: ResultDispatcherState, data: ResultDispatcher) => {
switch (type) { switch (data.type) {
case 'reset': { case 'reset': {
return initialSearchResultState; return initialSearchResultState;
} }
@ -150,24 +182,24 @@ function SearchPage() {
return {...prevState, loading: true}; return {...prevState, loading: true};
} }
case 'update': { case 'update': {
if (searchQuery !== state.query) { if (searchQuery !== data.value.query) {
return prevState; return prevState;
} }
return { return {
...state, ...data.value,
items: items:
state.lastPage === 0 data.value.lastPage === 0
? state.items ? data.value.items
: prevState.items.concat(state.items), : prevState.items.concat(data.value.items),
}; };
} }
case 'advance': { case 'advance': {
const hasMore = prevState.totalPages > prevState.lastPage + 1; const hasMore = prevState.totalPages! > prevState.lastPage! + 1;
return { return {
...prevState, ...prevState,
lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage, lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
hasMore, hasMore,
}; };
} }
@ -193,7 +225,7 @@ function SearchPage() {
return; return;
} }
const sanitizeValue = (value) => { const sanitizeValue = (value: string) => {
return value.replace( return value.replace(
/algolia-docsearch-suggestion--highlight/g, /algolia-docsearch-suggestion--highlight/g,
'search-result-match', 'search-result-match',
@ -212,7 +244,7 @@ function SearchPage() {
}); });
return { return {
title: titles.pop(), title: titles.pop()!,
url: isRegexpStringMatch(externalUrlRegex, parsedURL.href) url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
? parsedURL.href ? parsedURL.href
: parsedURL.pathname + parsedURL.hash, : parsedURL.pathname + parsedURL.hash,
@ -239,7 +271,7 @@ function SearchPage() {
}, },
); );
const [loaderRef, setLoaderRef] = useState(null); const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null);
const prevY = useRef(0); const prevY = useRef(0);
const observer = useRef( const observer = useRef(
ExecutionEnvironment.canUseDOM && ExecutionEnvironment.canUseDOM &&
@ -278,7 +310,7 @@ function SearchPage() {
description: 'The search page title for empty query', description: 'The search page title for empty query',
}); });
const makeSearch = useDynamicCallback((page = 0) => { const makeSearch = useDynamicCallback((page: number = 0) => {
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default'); algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
@ -299,9 +331,11 @@ function SearchPage() {
return undefined; return undefined;
} }
const currentObserver = observer.current; const currentObserver = observer.current;
if (currentObserver) {
currentObserver.observe(loaderRef); currentObserver.observe(loaderRef);
return () => currentObserver.unobserve(loaderRef); return () => currentObserver.unobserve(loaderRef);
}
return () => true;
}, [loaderRef]); }, [loaderRef]);
useEffect(() => { useEffect(() => {

View file

@ -5,10 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import useContextualSearchFilters from '@theme/hooks/useContextualSearchFilters'; import type {useAlgoliaContextualFacetFiltersReturns} from '@theme/hooks/useAlgoliaContextualFacetFilters';
import {useContextualSearchFilters} from '@docusaurus/theme-common';
// Translate search-engine agnostic search filters to Algolia search filters // Translate search-engine agnostic search filters to Algolia search filters
export default function useAlgoliaContextualFacetFilters() { export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns {
const {locale, tags} = useContextualSearchFilters(); const {locale, tags} = useContextualSearchFilters();
// seems safe to convert locale->language, see AlgoliaSearchMetadata comment // seems safe to convert locale->language, see AlgoliaSearchMetadata comment

View file

@ -8,10 +8,11 @@
import {useHistory} from '@docusaurus/router'; import {useHistory} from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useCallback, useEffect, useState} from 'react'; import {useCallback, useEffect, useState} from 'react';
import type {SearchQuery} from '@theme/hooks/useSearchQuery';
const SEARCH_PARAM_QUERY = 'q'; const SEARCH_PARAM_QUERY = 'q';
function useSearchQuery() { function useSearchQuery(): SearchQuery {
const history = useHistory(); const history = useHistory();
const { const {
siteConfig: {baseUrl}, siteConfig: {baseUrl},
@ -28,7 +29,7 @@ function useSearchQuery() {
}, []); }, []);
const setSearchQuery = useCallback( const setSearchQuery = useCallback(
(newSearchQuery) => { (newSearchQuery: string) => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
if (newSearchQuery) { if (newSearchQuery) {
@ -46,7 +47,7 @@ function useSearchQuery() {
); );
const generateSearchPageLink = useCallback( const generateSearchPageLink = useCallback(
(targetSearchQuery) => { (targetSearchQuery: string) => {
// Refer to https://github.com/facebook/docusaurus/pull/2838 // Refer to https://github.com/facebook/docusaurus/pull/2838
return `${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`; return `${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`;
}, },

View file

@ -0,0 +1,10 @@
/**
* 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.
*/
/// <reference types="@docusaurus/module-type-aliases" />
/// <reference types="@docusaurus/theme-common" />
/// <reference types="@docusaurus/theme-classic" />

View file

@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
const {Joi} = require('@docusaurus/utils-validation'); import {Joi} from '@docusaurus/utils-validation';
import type {ThemeConfig, Validate, ValidationResult} from '@docusaurus/types';
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
contextualSearch: false, // future: maybe we want to enable this by default contextualSearch: false, // future: maybe we want to enable this by default
@ -18,7 +19,7 @@ const DEFAULT_CONFIG = {
}; };
exports.DEFAULT_CONFIG = DEFAULT_CONFIG; exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
const Schema = Joi.object({ export const Schema = Joi.object({
algolia: Joi.object({ algolia: Joi.object({
// Docusaurus attributes // Docusaurus attributes
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch), contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
@ -35,11 +36,13 @@ const Schema = Joi.object({
.required() .required()
.unknown(), // DocSearch 3 is still alpha: don't validate the rest for now .unknown(), // DocSearch 3 is still alpha: don't validate the rest for now
}); });
exports.Schema = Schema;
exports.validateThemeConfig = function validateThemeConfig({ export function validateThemeConfig({
validate, validate,
themeConfig, themeConfig,
}) { }: {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
return validate(Schema, themeConfig); return validate(Schema, themeConfig);
}; }

View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"jsx": "react-native"
},
"include": ["src/theme/", "src/*.d.ts"]
}

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "ES2019"],
"rootDir": "src",
"baseUrl": "src",
"outDir": "lib"
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["src/*.ts", "src/templates/*.ts"]
}