feat(v2): Support swizzling TypeScript components (#2671)

* feat(v2): Support swizzling TypeScript components

* Add tsc --noEmit to tsc script in theme-classic

Now everything can pass the type checker! (although still a lot of any)

* Add tsconfig and types.d.ts to website

Improve developer experience.

As an example, I converted NotFound to tsx

* Apply type annotation suggestions

* Do not fallback to `getThemePath` if getTypeScriptThemePath is undefined

* Fix tsc

* Add module declaration for @theme-original/*

* Move babel cli to root package.json
This commit is contained in:
Sam Zhou 2020-06-25 10:07:30 -04:00 committed by GitHub
parent 20930dc837
commit 5ccd24cc1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 345 additions and 108 deletions

View file

@ -22,6 +22,7 @@ packages/docusaurus-plugin-debug/lib/
packages/docusaurus-plugin-sitemap/lib/
packages/docusaurus-plugin-ideal-image/lib/
packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js
packages/docusaurus-theme-classic/lib/
packages/docusaurus-1.x/.eslintrc.js
packages/docusaurus-init/templates/facebook/.eslintrc.js

1
.gitignore vendored
View file

@ -26,3 +26,4 @@ packages/docusaurus-plugin-content-pages/lib/
packages/docusaurus-plugin-debug/lib/
packages/docusaurus-plugin-sitemap/lib/
packages/docusaurus-plugin-ideal-image/lib/
packages/docusaurus-theme-classic/lib/

View file

@ -34,6 +34,7 @@
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/cli": "^7.9.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-typescript": "^7.9.0",
@ -52,6 +53,7 @@
"@types/lodash.pick": "^4.4.6",
"@types/lodash.pickby": "^4.6.6",
"@types/node": "^13.11.0",
"@types/prismjs": "^1.16.1",
"@types/react": "^16.9.38",
"@types/react-dev-utils": "^9.0.1",
"@types/react-helmet": "^6.0.0",

View file

@ -37,10 +37,9 @@ declare module '@generated/routesChunkNames' {
export default routesChunkNames;
}
declare module '@theme/*' {
const component: any;
export default component;
}
declare module '@theme/*';
declare module '@theme-original/*';
declare module '@docusaurus/*';

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.
*/
module.exports = {
presets: [['@babel/preset-typescript', {isTSX: true, allExtensions: true}]],
};

View file

@ -7,6 +7,11 @@
"access": "public"
},
"license": "MIT",
"scripts": {
"tsc": "tsc --noEmit && yarn babel && yarn prettier",
"babel": "babel src -d lib --extensions \".tsx,.ts\" --copy-files",
"prettier": "prettier --config ../../.prettierrc --write \"**/*.{js,ts}\""
},
"dependencies": {
"@hapi/joi": "^17.1.1",
"@mdx-js/mdx": "^1.5.8",
@ -21,6 +26,9 @@
"react-router-dom": "^5.1.2",
"react-toggle": "^4.1.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.0.0-alpha.58"
},
"peerDependencies": {
"@docusaurus/core": "^2.0.0",
"react": "^16.8.4",

View file

@ -61,6 +61,10 @@ module.exports = function (context, options) {
name: 'docusaurus-theme-classic',
getThemePath() {
return path.join(__dirname, '..', 'lib', 'theme');
},
getTypeScriptThemePath() {
return path.resolve(__dirname, './theme');
},

View file

@ -11,9 +11,9 @@ import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
import styles from './styles.module.css';
function AnnouncementBar() {
function AnnouncementBar(): JSX.Element | null {
const {
siteConfig: {themeConfig: {announcementBar = {}}} = {},
siteConfig: {themeConfig: {announcementBar = {}} = {}} = {},
} = useDocusaurusContext();
const {content, backgroundColor, textColor} = announcementBar;
const {

View file

@ -12,7 +12,12 @@ import Layout from '@theme/Layout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogListPaginator from '@theme/BlogListPaginator';
function BlogListPage(props) {
type Props = {
metadata: {permalink: string; title: string};
items: {content}[];
};
function BlogListPage(props: Props): JSX.Element {
const {metadata, items} = props;
const {
siteConfig: {title: siteTitle},

View file

@ -8,7 +8,7 @@
import React from 'react';
import Link from '@docusaurus/Link';
function BlogListPaginator(props) {
function BlogListPaginator(props): JSX.Element {
const {metadata} = props;
const {previousPage, nextPage} = metadata;

View file

@ -31,7 +31,7 @@ const MONTHS = [
'December',
];
function BlogPostItem(props) {
function BlogPostItem(props): JSX.Element {
const {
children,
frontMatter,

View file

@ -11,7 +11,7 @@ import Layout from '@theme/Layout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
function BlogPostPage(props) {
function BlogPostPage(props): JSX.Element {
const {content: BlogPostContents} = props;
const {frontMatter, metadata} = BlogPostContents;
const {title, description, nextItem, prevItem, editUrl} = metadata;

View file

@ -8,7 +8,7 @@
import React from 'react';
import Link from '@docusaurus/Link';
function BlogPostPaginator(props) {
function BlogPostPaginator(props): JSX.Element {
const {nextItem, prevItem} = props;
return (

View file

@ -10,15 +10,17 @@ import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
function getCategoryOfTag(tag) {
function getCategoryOfTag(tag: string) {
// tag's category should be customizable
return tag[0].toUpperCase();
}
function BlogTagsListPage(props) {
type Tag = {permalink: string; name: string; count: number};
function BlogTagsListPage(props: {tags: Record<string, Tag>}): JSX.Element {
const {tags} = props;
const tagCategories = {};
const tagCategories: {[category: string]: string[]} = {};
Object.keys(tags).forEach((tag) => {
const category = getCategoryOfTag(tag);
tagCategories[category] = tagCategories[category] || [];

View file

@ -11,11 +11,11 @@ import Layout from '@theme/Layout';
import BlogPostItem from '@theme/BlogPostItem';
import Link from '@docusaurus/Link';
function pluralize(count, word) {
function pluralize(count: number, word: string) {
return count > 1 ? `${word}s` : word;
}
function BlogTagsPostPage(props) {
function BlogTagsPostPage(props): JSX.Element {
const {metadata, items} = props;
const {allTagsPath, name: tagName, count} = metadata;

View file

@ -87,7 +87,15 @@ const highlightDirectiveRegex = (lang) => {
};
const codeBlockTitleRegex = /title=".*"/;
export default ({children, className: languageClassName, metastring}) => {
export default ({
children,
className: languageClassName,
metastring,
}: {
children: string;
className: string;
metastring: string;
}): JSX.Element => {
const {
siteConfig: {
themeConfig: {prism = {}},
@ -108,21 +116,25 @@ export default ({children, className: languageClassName, metastring}) => {
}, []);
const button = useRef(null);
let highlightLines = [];
let highlightLines: number[] = [];
let codeBlockTitle = '';
const prismTheme = usePrismTheme();
if (metastring && highlightLinesRangeRegex.test(metastring)) {
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)[1];
// Tested above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)![1];
highlightLines = rangeParser
.parse(highlightLinesRange)
.filter((n) => n > 0);
}
if (metastring && codeBlockTitleRegex.test(metastring)) {
// Tested above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
codeBlockTitle = metastring
.match(codeBlockTitleRegex)[0]
.match(codeBlockTitleRegex)![0]
.split('title=')[1]
.replace(/"+/g, '');
}
@ -151,7 +163,10 @@ export default ({children, className: languageClassName, metastring}) => {
if (match !== null) {
const directive = match
.slice(1)
.reduce((final, item) => final || item, undefined);
.reduce(
(final: string | undefined, item) => final || item,
undefined,
);
switch (directive) {
case 'highlight-next-line':
range += `${lineNumber},`;
@ -188,9 +203,10 @@ export default ({children, className: languageClassName, metastring}) => {
return (
<Highlight
{...defaultProps}
key={mounted}
key={String(mounted)}
theme={prismTheme}
code={code}
// @ts-expect-error: prism-react-renderer doesn't export Language type
language={language}>
{({className, style, tokens, getLineProps, getTokenProps}) => (
<>
@ -211,7 +227,7 @@ export default ({children, className: languageClassName, metastring}) => {
{showCopied ? 'Copied' : 'Copy'}
</button>
<div
tabIndex="0"
tabIndex={0}
className={clsx(className, styles.codeBlock, {
[styles.codeBlockWithTitle]: codeBlockTitle,
})}>

View file

@ -33,7 +33,7 @@ function DocTOC({headings}) {
}
/* eslint-disable jsx-a11y/control-has-associated-label */
function Headings({headings, isChild}) {
function Headings({headings, isChild}: {headings; isChild?: boolean}) {
if (!headings.length) {
return null;
}
@ -58,7 +58,7 @@ function Headings({headings, isChild}) {
);
}
function DocItem(props) {
function DocItem(props): JSX.Element {
const {siteConfig = {}} = useDocusaurusContext();
const {url: siteUrl, title: siteTitle} = siteConfig;
const {content: DocContent} = props;

View file

@ -18,7 +18,7 @@ import {matchPath} from '@docusaurus/router';
import styles from './styles.module.css';
function DocPage(props) {
function DocPage(props): JSX.Element {
const {route: baseRoute, docsMetadata, location} = props;
// case-sensitive route such as it is defined in the sidebar
const currentRoute =

View file

@ -8,7 +8,13 @@
import React from 'react';
import Link from '@docusaurus/Link';
function DocPaginator(props) {
type PageInfo = {permalink: string; title: string};
type Props = {
metadata: {previous: PageInfo; next: PageInfo};
};
function DocPaginator(props: Props): JSX.Element {
const {metadata} = props;
return (

View file

@ -163,11 +163,11 @@ function DocSidebarItem(props) {
}
}
function DocSidebar(props) {
function DocSidebar(props): JSX.Element | null {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const {
siteConfig: {
themeConfig: {navbar: {title, hideOnScroll = false} = {}},
themeConfig: {navbar: {title = '', hideOnScroll = false} = {}} = {},
} = {},
isClient,
} = useDocusaurusContext();

View file

@ -39,7 +39,7 @@ const FooterLogo = ({url, alt}) => (
<img className="footer__logo" alt={alt} src={url} />
);
function Footer() {
function Footer(): JSX.Element | null {
const context = useDocusaurusContext();
const {siteConfig = {}} = context;
const {themeConfig = {}} = siteConfig;

View file

@ -7,14 +7,14 @@
/* eslint-disable jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid */
import React from 'react';
import React, {ComponentType} from 'react';
import clsx from 'clsx';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import './styles.css';
import styles from './styles.module.css';
const Heading = (Tag) =>
const Heading = (Tag: ComponentType): ((props) => JSX.Element) =>
function TargetComponent({id, ...props}) {
const {
siteConfig: {
@ -30,7 +30,7 @@ const Heading = (Tag) =>
<Tag {...props}>
<a
aria-hidden="true"
tabIndex="-1"
tabIndex={-1}
className={clsx('anchor', {
[styles.enhancedAnchor]: !hideOnScroll,
})}
@ -39,7 +39,7 @@ const Heading = (Tag) =>
{props.children}
<a
aria-hidden="true"
tabIndex="-1"
tabIndex={-1}
className="hash-link"
href={`#${id}`}
title="Direct link to heading">

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ReactNode} from 'react';
import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
@ -18,7 +18,18 @@ import Footer from '@theme/Footer';
import './styles.css';
function Layout(props) {
type Props = {
children: ReactNode;
title?: string;
noFooter?: boolean;
description?: string;
image?: string;
keywords?: string[];
permalink?: string;
version?: string;
};
function Layout(props: Props): JSX.Element {
const {siteConfig = {}} = useDocusaurusContext();
const {
favicon,

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ComponentProps} from 'react';
import Link from '@docusaurus/Link';
import CodeBlock from '@theme/CodeBlock';
import Heading from '@theme/Heading';
@ -13,7 +13,7 @@ import Heading from '@theme/Heading';
import styles from './styles.module.css';
export default {
code: (props) => {
code: (props: ComponentProps<typeof CodeBlock>): JSX.Element => {
const {children} = props;
if (typeof children === 'string') {
if (!children.includes('\n')) {
@ -23,14 +23,16 @@ export default {
}
return children;
},
a: (props) => {
if (/\.[^./]+$/.test(props.href)) {
a: (props: ComponentProps<'a'>): JSX.Element => {
if (/\.[^./]+$/.test(props.href || '')) {
// eslint-disable-next-line jsx-a11y/anchor-has-content
return <a {...props} />;
}
return <Link {...props} />;
},
pre: (props) => <div className={styles.mdxCodeBlock} {...props} />,
pre: (props: ComponentProps<'div'>): JSX.Element => (
<div className={styles.mdxCodeBlock} {...props} />
),
h1: Heading('h1'),
h2: Heading('h2'),
h3: Heading('h3'),

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {useCallback, useState, useEffect} from 'react';
import React, {useCallback, useState, useEffect, ComponentProps} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@ -33,7 +33,15 @@ function NavLink({
activeClassName = 'navbar__link--active',
prependBaseUrlToHref,
...props
}) {
}: {
activeBasePath?: string;
activeBaseRegex?: string;
to?: string;
href?: string;
label?: string;
activeClassName?: string;
prependBaseUrlToHref?: string;
} & ComponentProps<'a'>) {
const toUrl = useBaseUrl(to);
const activeBaseUrl = useBaseUrl(activeBasePath);
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
@ -96,7 +104,8 @@ function NavItem({
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.target.parentNode.classList.toggle('dropdown--show');
((e.target as HTMLElement)
.parentNode as HTMLElement).classList.toggle('dropdown--show');
}
}}>
{props.label}
@ -171,11 +180,11 @@ function splitLinks(links) {
};
}
function Navbar() {
function Navbar(): JSX.Element {
const {
siteConfig: {
themeConfig: {
navbar: {title, links = [], hideOnScroll = false} = {},
navbar: {title = '', links = [], hideOnScroll = false} = {},
disableDarkMode = false,
},
},

View file

@ -8,7 +8,7 @@
import React from 'react';
import Layout from '@theme/Layout';
function NotFound() {
function NotFound(): JSX.Element {
return (
<Layout title="Page Not Found">
<div className="container margin-vert--xl">

View file

@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ReactNode} from 'react';
function TabItem(props) {
function TabItem(props: {readonly children: ReactNode}): JSX.Element {
return <div>{props.children}</div>;
}

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {useState, Children} from 'react';
import React, {useState, Children, ReactElement} from 'react';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
import clsx from 'clsx';
@ -17,7 +17,15 @@ const keys = {
right: 39,
};
function Tabs(props) {
type Props = {
block?: boolean;
children: ReactElement<{value: string}>[];
defaultValue?: string;
values: {value: string; label: string}[];
groupId?: string;
};
function Tabs(props: Props): JSX.Element {
const {block, children, defaultValue, values, groupId} = props;
const {tabGroupChoices, setTabGroupChoices} = useUserPreferencesContext();
const [selectedValue, setSelectedValue] = useState(defaultValue);
@ -40,7 +48,7 @@ function Tabs(props) {
}
};
const tabRefs = [];
const tabRefs: (HTMLLIElement | null)[] = [];
const focusNextTab = (tabs, target) => {
const next = tabs.indexOf(target) + 1;
@ -86,7 +94,7 @@ function Tabs(props) {
{values.map(({value, label}) => (
<li
role="tab"
tabIndex="0"
tabIndex={0}
aria-selected={selectedValue === value}
className={clsx('tabs__item', styles.tabItem, {
'tabs__item--active': selectedValue === value,
@ -103,7 +111,9 @@ function Tabs(props) {
<div role="tabpanel" className="margin-vert--md">
{
Children.toArray(children).filter(
(child) => child.props.value === selectedValue,
(child) =>
(child as ReactElement<{value: string}>).props.value ===
selectedValue,
)[0]
}
</div>

View file

@ -7,6 +7,6 @@
import React from 'react';
const ThemeContext = React.createContext();
const ThemeContext = React.createContext(undefined);
export default ThemeContext;

View file

@ -5,12 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ReactNode} from 'react';
import useTheme from '@theme/hooks/useTheme';
import ThemeContext from '@theme/ThemeContext';
function ThemeProvider(props) {
function ThemeProvider(props: {readonly children: ReactNode}): JSX.Element {
const {isDarkTheme, setLightTheme, setDarkTheme} = useTheme();
return (

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ComponentProps} from 'react';
import Toggle from 'react-toggle';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@ -16,7 +16,7 @@ import styles from './styles.module.css';
const Moon = () => <span className={clsx(styles.toggle, styles.moon)} />;
const Sun = () => <span className={clsx(styles.toggle, styles.sun)} />;
export default function (props) {
export default function (props: ComponentProps<typeof Toggle>): JSX.Element {
const {isClient} = useDocusaurusContext();
return (
<Toggle

View file

@ -7,6 +7,6 @@
import {createContext} from 'react';
const UserPreferencesContext = createContext();
const UserPreferencesContext = createContext(undefined);
export default UserPreferencesContext;

View file

@ -5,13 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {ReactNode} from 'react';
import useTabGroupChoice from '@theme/hooks/useTabGroupChoice';
import useAnnouncementBar from '@theme/hooks/useAnnouncementBar';
import UserPreferencesContext from '@theme/UserPreferencesContext';
function UserPreferencesProvider(props) {
function UserPreferencesProvider(props: {children: ReactNode}): JSX.Element {
const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
const {isAnnouncementBarClosed, closeAnnouncementBar} = useAnnouncementBar();

View file

@ -11,13 +11,18 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
const STORAGE_DISMISS_KEY = 'docusaurus.announcement.dismiss';
const STORAGE_ID_KEY = 'docusaurus.announcement.id';
const useAnnouncementBar = () => {
const useAnnouncementBar = (): {
isAnnouncementBarClosed: boolean;
closeAnnouncementBar: () => void;
} => {
const {
siteConfig: {themeConfig: {announcementBar: {id} = {}}} = {},
siteConfig: {
themeConfig: {announcementBar: {id = 'annoucement-bar'} = {}} = {},
} = {},
} = useDocusaurusContext();
const [isClosed, setClosed] = useState(true);
const handleClose = () => {
localStorage.setItem(STORAGE_DISMISS_KEY, true);
localStorage.setItem(STORAGE_DISMISS_KEY, 'true');
setClosed(true);
};
@ -32,7 +37,7 @@ const useAnnouncementBar = () => {
localStorage.setItem(STORAGE_ID_KEY, id);
if (isNewAnnouncement) {
localStorage.setItem(STORAGE_DISMISS_KEY, false);
localStorage.setItem(STORAGE_DISMISS_KEY, 'false');
}
if (

View file

@ -10,7 +10,7 @@ import {useLocation} from '@docusaurus/router';
import useLocationHash from '@theme/hooks/useLocationHash';
import useScrollPosition from '@theme/hooks/useScrollPosition';
const useHideableNavbar = (hideOnScroll) => {
const useHideableNavbar = (hideOnScroll: boolean) => {
const [isNavbarVisible, setIsNavbarVisible] = useState(true);
const [isFocusedAnchor, setIsFocusedAnchor] = useState(false);
const [lastScrollTop, setLastScrollTop] = useState(0);

View file

@ -5,9 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {useState, useEffect} from 'react';
import {useState, useEffect, Dispatch, SetStateAction} from 'react';
function useLocationHash(initialHash) {
function useLocationHash(
initialHash: string,
): [string, Dispatch<SetStateAction<string>>] {
const [hash, setHash] = useState(initialHash);
useEffect(() => {

View file

@ -7,7 +7,7 @@
import {useEffect} from 'react';
function useLockBodyScroll(lock = true) {
function useLockBodyScroll(lock: boolean = true): void {
useEffect(() => {
document.body.style.overflow = lock ? 'hidden' : 'visible';

View file

@ -10,13 +10,20 @@ import useThemeContext from '@theme/hooks/useThemeContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import isInternalUrl from '@docusaurus/isInternalUrl';
const useLogo = () => {
type LogoLinkProps = {target?: string; rel?: string};
const useLogo = (): {
logoLink: string;
logoLinkProps: LogoLinkProps;
logoImageUrl: string;
logoAlt: string;
} => {
const {
siteConfig: {themeConfig: {navbar: {logo = {}} = {}}} = {},
siteConfig: {themeConfig: {navbar: {logo = {}} = {}} = {}} = {},
} = useDocusaurusContext();
const {isDarkTheme} = useThemeContext();
const logoLink = useBaseUrl(logo.href || '/');
let logoLinkProps = {};
let logoLinkProps: LogoLinkProps = {};
if (logo.target) {
logoLinkProps = {target: logo.target};

View file

@ -9,7 +9,7 @@ import defaultTheme from 'prism-react-renderer/themes/palenight';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useThemeContext from '@theme/hooks/useThemeContext';
const usePrismTheme = () => {
const usePrismTheme = (): typeof defaultTheme => {
const {
siteConfig: {
themeConfig: {prism = {}},

View file

@ -8,12 +8,17 @@
import {useState, useEffect} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
const getScrollPosition = () => ({
type ScrollPosition = {scrollX: number; scrollY: number};
const getScrollPosition = (): ScrollPosition => ({
scrollX: ExecutionEnvironment.canUseDOM ? window.pageXOffset : 0,
scrollY: ExecutionEnvironment.canUseDOM ? window.pageYOffset : 0,
});
const useScrollPosition = (effect, deps = []) => {
const useScrollPosition = (
effect: (position: ScrollPosition) => void,
deps = [],
): ScrollPosition => {
const [scrollPosition, setScrollPosition] = useState(getScrollPosition());
const handleScroll = () => {
@ -31,6 +36,7 @@ const useScrollPosition = (effect, deps = []) => {
return () =>
window.removeEventListener('scroll', handleScroll, {
// @ts-expect-error: See https://github.com/microsoft/TypeScript/issues/32912
passive: true,
});
}, deps);

View file

@ -7,17 +7,23 @@
import {useEffect, useState} from 'react';
function useTOCHighlight(linkClassName, linkActiveClassName, topOffset) {
const [lastActiveLink, setLastActiveLink] = useState(undefined);
function useTOCHighlight(
linkClassName: string,
linkActiveClassName: string,
topOffset: number,
): void {
const [lastActiveLink, setLastActiveLink] = useState<HTMLAnchorElement>(
undefined!,
);
useEffect(() => {
let headersAnchors = [];
let links = [];
let headersAnchors: HTMLCollectionOf<Element>;
let links: HTMLCollectionOf<HTMLAnchorElement>;
function setActiveLink() {
function getActiveHeaderAnchor() {
let index = 0;
let activeHeaderAnchor = null;
let activeHeaderAnchor: Element | null = null;
headersAnchors = document.getElementsByClassName('anchor');
while (index < headersAnchors.length && !activeHeaderAnchor) {
@ -40,6 +46,7 @@ function useTOCHighlight(linkClassName, linkActiveClassName, topOffset) {
let index = 0;
let itemHighlighted = false;
// @ts-expect-error: Must be <a> tags.
links = document.getElementsByClassName(linkClassName);
while (index < links.length && !itemHighlighted) {
const link = links[index];

View file

@ -9,8 +9,13 @@ import {useState, useCallback, useEffect} from 'react';
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
const useTabGroupChoice = () => {
const [tabGroupChoices, setChoices] = useState({});
const useTabGroupChoice = (): {
tabGroupChoices: {readonly [groupId: string]: string};
setTabGroupChoices: (groupId: string, newChoice: string) => void;
} => {
const [tabGroupChoices, setChoices] = useState<{
readonly [groupId: string]: string;
}>({});
const setChoiceSyncWithLocalStorage = useCallback((groupId, newChoice) => {
try {
localStorage.setItem(`${TAB_CHOICE_PREFIX}${groupId}`, newChoice);
@ -23,7 +28,7 @@ const useTabGroupChoice = () => {
try {
const localStorageChoices = {};
for (let i = 0; i < localStorage.length; i += 1) {
const storageKey = localStorage.key(i);
const storageKey = localStorage.key(i) as string;
if (storageKey.startsWith(TAB_CHOICE_PREFIX)) {
const groupId = storageKey.substring(TAB_CHOICE_PREFIX.length);
localStorageChoices[groupId] = localStorage.getItem(storageKey);
@ -37,7 +42,7 @@ const useTabGroupChoice = () => {
return {
tabGroupChoices,
setTabGroupChoices: (groupId, newChoice) => {
setTabGroupChoices: (groupId: string, newChoice: string) => {
setChoices((oldChoices) => ({...oldChoices, [groupId]: newChoice}));
setChoiceSyncWithLocalStorage(groupId, newChoice);
},

View file

@ -14,9 +14,13 @@ const themes = {
dark: 'dark',
};
const useTheme = () => {
const useTheme = (): {
isDarkTheme: boolean;
setLightTheme: () => void;
setDarkTheme: () => void;
} => {
const {
siteConfig: {themeConfig: {disableDarkMode}} = {},
siteConfig: {themeConfig: {disableDarkMode = false} = {}} = {},
} = useDocusaurusContext();
const [theme, setTheme] = useState(
typeof document !== 'undefined'
@ -43,6 +47,7 @@ const useTheme = () => {
}, []);
useEffect(() => {
// @ts-expect-error: safe to set null as attribute
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);

View file

@ -9,8 +9,14 @@ import {useContext} from 'react';
import ThemeContext from '@theme/ThemeContext';
function useThemeContext() {
const context = useContext(ThemeContext);
type ThemeContextProps = {
isDarkTheme: boolean;
setLightTheme: () => void;
setDarkTheme: () => void;
};
function useThemeContext(): ThemeContextProps {
const context = useContext<ThemeContextProps>(ThemeContext);
if (context == null) {
throw new Error(
'`useThemeContext` is used outside of `Layout` Component. See https://v2.docusaurus.io/docs/theme-classic#usethemecontext.',

View file

@ -9,8 +9,17 @@ import {useContext} from 'react';
import UserPreferencesContext from '@theme/UserPreferencesContext';
function useUserPreferencesContext() {
const context = useContext(UserPreferencesContext);
type UserPreferencesContextProps = {
tabGroupChoices: {readonly [groupId: string]: string};
setTabGroupChoices: (groupId: string, newChoice: string) => void;
isAnnouncementBarClosed: boolean;
closeAnnouncementBar: () => void;
};
function useUserPreferencesContext(): UserPreferencesContextProps {
const context = useContext<UserPreferencesContextProps>(
UserPreferencesContext,
);
if (context == null) {
throw new Error(
'`useUserPreferencesContext` is used outside of `Layout` Component.',

View file

@ -12,9 +12,11 @@ const desktopThresholdWidth = 996;
const windowSizes = {
desktop: 'desktop',
mobile: 'mobile',
};
} as const;
function useWindowSize() {
type WindowSize = keyof typeof windowSizes;
function useWindowSize(): WindowSize | undefined {
const isClient = typeof window !== 'undefined';
function getSize() {
@ -30,7 +32,7 @@ function useWindowSize() {
useEffect(() => {
if (!isClient) {
return false;
return undefined;
}
function handleResize() {

View file

@ -7,16 +7,17 @@
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import siteConfig from '@generated/docusaurus.config';
import type * as PrismNamespace from 'prismjs';
const prismIncludeLanguages = (Prism) => {
const prismIncludeLanguages = (PrismObject: typeof PrismNamespace): void => {
if (ExecutionEnvironment.canUseDOM) {
const {
themeConfig: {prism: {additionalLanguages = []} = {}},
} = siteConfig;
window.Prism = Prism;
window.Prism = PrismObject;
additionalLanguages.forEach((lang) => {
additionalLanguages.forEach((lang: string) => {
require(`prismjs/components/prism-${lang}`); // eslint-disable-line
});

View file

@ -0,0 +1,9 @@
/**
* 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-next-line spaced-comment
/// <reference types="@docusaurus/module-type-aliases" />

View file

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["DOM"],
"module": "esnext",
"noEmit": true,
"noImplicitAny": false,
"jsx": "react",
"baseUrl": "src"
},
"include": ["src/"]
}

View file

@ -120,6 +120,7 @@ export interface Plugin<T, U = unknown> {
utils: ConfigureWebpackUtils,
): Configuration;
getThemePath?(): string;
getTypeScriptThemePath?(): string;
getPathsToWatch?(): string[];
getClientModules?(): string[];
extendCli?(cli: Command): void;

View file

@ -60,7 +60,7 @@ export function objectWithKeySorted(obj: {[index: string]: any}) {
}
const indexRE = /(^|.*\/)index\.(md|js|jsx|ts|tsx)$/i;
const extRE = /\.(md|js|tsx)$/;
const extRE = /\.(md|js|ts|tsx)$/;
/**
* Convert filepath to url path.

View file

@ -60,8 +60,17 @@ cli
cli
.command('swizzle <themeName> [componentName] [siteDir]')
.description('Copy the theme files into website folder for customization.')
.action((themeName, componentName, siteDir = '.') => {
wrapCommand(swizzle)(path.resolve(siteDir), themeName, componentName);
.option(
'--typescript',
'Copy TypeScript theme files when possible (default: false)',
)
.action((themeName, componentName, siteDir = '.', {typescript}) => {
wrapCommand(swizzle)(
path.resolve(siteDir),
themeName,
componentName,
typescript,
);
});
cli

View file

@ -18,13 +18,16 @@ export default async function swizzle(
siteDir: string,
themeName: string,
componentName?: string,
typescript?: boolean,
): Promise<void> {
const plugin = importFresh(themeName) as (
context: LoadContext,
) => Plugin<unknown>;
const context = loadContext(siteDir);
const pluginInstance = plugin(context);
let fromPath = pluginInstance.getThemePath?.();
let fromPath = typescript
? pluginInstance.getTypeScriptThemePath?.()
: pluginInstance.getThemePath?.();
if (fromPath) {
let toPath = path.resolve(siteDir, THEME_PATH);
@ -32,10 +35,16 @@ export default async function swizzle(
fromPath = path.join(fromPath, componentName);
toPath = path.join(toPath, componentName);
// Handle single JavaScript file only.
// E.g: if <fromPath> does not exist, we try to swizzle <fromPath>.js instead
if (!fs.existsSync(fromPath) && fs.existsSync(`${fromPath}.js`)) {
[fromPath, toPath] = [`${fromPath}.js`, `${toPath}.js`];
// Handle single TypeScript/JavaScript file only.
// E.g: if <fromPath> does not exist, we try to swizzle <fromPath>.(ts|tsx|js) instead
if (!fs.existsSync(fromPath)) {
if (fs.existsSync(`${fromPath}.ts`)) {
[fromPath, toPath] = [`${fromPath}.ts`, `${toPath}.ts`];
} else if (fs.existsSync(`${fromPath}.tsx`)) {
[fromPath, toPath] = [`${fromPath}.tsx`, `${toPath}.tsx`];
} else if (fs.existsSync(`${fromPath}.js`)) {
[fromPath, toPath] = [`${fromPath}.js`, `${toPath}.js`];
}
}
}
await fs.copy(fromPath, toPath);
@ -48,5 +57,13 @@ export default async function swizzle(
console.log(
`\n${chalk.green('Success!')} Copied ${fromMsg} to ${toMsg}.\n`,
);
} else if (typescript) {
console.warn(
chalk.yellow(
`${themeName} does not provide TypeScript theme code via getTypeScriptThemePath().`,
),
);
} else {
console.warn(chalk.yellow(`${themeName} does not provide any theme code.`));
}
}

View file

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es2017","es2019.array"],
"lib": ["es2017","es2019.array", "DOM"],
"declaration": true,
"declarationMap": true,

View file

@ -9,7 +9,7 @@ import React from 'react';
import Layout from '@theme/Layout';
import Feedback from '../pages/feedback';
function NotFound({location}) {
function NotFound({location}: {location: {pathname: string}}): JSX.Element {
if (/^\/feedback/.test(location.pathname)) {
return <Feedback />;
}

9
website/src/types.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/**
* 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-next-line spaced-comment
/// <reference types="@docusaurus/module-type-aliases" />

13
website/tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["DOM"],
"module": "esnext",
"noEmit": true,
"noImplicitAny": false,
"jsx": "react",
"baseUrl": "src"
},
"include": ["src/"]
}

View file

@ -2,6 +2,22 @@
# yarn lockfile v1
"@babel/cli@^7.9.0":
version "7.10.3"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.3.tgz#4ea83bd997d2a41c78d07263ada3ec466fb3764b"
integrity sha512-lWB3yH5/fWY8pi2Kj5/fA+17guJ9feSBw5DNjTju3/nRi9sXnl1JPh7aKQOSvdNbiDbkzzoGYtsr46M8dGmXDQ==
dependencies:
commander "^4.0.1"
convert-source-map "^1.1.0"
fs-readdir-recursive "^1.1.0"
glob "^7.0.0"
lodash "^4.17.13"
make-dir "^2.1.0"
slash "^2.0.0"
source-map "^0.5.0"
optionalDependencies:
chokidar "^2.1.8"
"@babel/code-frame@7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@ -3051,6 +3067,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
"@types/prismjs@^1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.1.tgz#50b82947207847db6abcbcd14caa89e3b897c259"
integrity sha512-RNgcK3FEc1GpeOkamGDq42EYkb6yZW5OWQwTS56NJIB8WL0QGISQglA7En7NUx9RGP8AC52DOe+squqbAckXlA==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@ -5807,7 +5828,7 @@ conventional-recommended-bump@^5.0.0:
meow "^4.0.0"
q "^1.5.1"
convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
@ -8301,6 +8322,11 @@ fs-minipass@^2.0.0:
dependencies:
minipass "^3.0.0"
fs-readdir-recursive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"