feat: properly type-check the Docusaurus config of new sites (#5589)

* fix: makes types DocusaurusConfig optional to match docs

* add UserDocusaurusConfig with required keys for user config

* convert UserDocusaurusConfig to use util type

* Docusaurus website config should be type-checked by CI + fix all existing issues

* add doc for config typechecking

* Update template configs for TS autocompletion

* fix last config typechecking bugs

* reapply prettier

* reapply prettier-docs

* Fix TS doc: add missing ()

* fix some docu plugin types

* add "const config" for simpler jsdoc annotation

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Bharat Middha 2021-09-30 08:49:44 -07:00 committed by GitHub
parent 3f1f8255a2
commit 09550b0535
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 115 additions and 35 deletions

View file

@ -51,7 +51,7 @@
"lerna": "lerna", "lerna": "lerna",
"test": "cross-env TZ=UTC jest", "test": "cross-env TZ=UTC jest",
"test:build:website": "./admin/scripts/test-release.sh", "test:build:website": "./admin/scripts/test-release.sh",
"watch": "yarn lerna run --parallel --no-private watch", "watch": "yarn lerna run --parallel watch",
"clear": "(yarn workspace website clear || echo 'Failure while running docusaurus clear') && yarn lerna exec --ignore docusaurus yarn rimraf lib lib-next", "clear": "(yarn workspace website clear || echo 'Failure while running docusaurus clear') && yarn lerna exec --ignore docusaurus yarn rimraf lib lib-next",
"test:baseUrl": "yarn build:website:baseUrl && yarn serve:website:baseUrl", "test:baseUrl": "yarn build:website:baseUrl && yarn serve:website:baseUrl",
"lock:update": "npx yarn-deduplicate" "lock:update": "npx yarn-deduplicate"

View file

@ -1,5 +1,8 @@
/** @type {import('@docusaurus/types').DocusaurusConfig} */ // @ts-check
module.exports = { // Note: type annotations allow type checking and IDEs autocompletion
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'My Site', title: 'My Site',
tagline: 'The tagline of my site', tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com', url: 'https://your-docusaurus-test-site.com',
@ -86,3 +89,5 @@ module.exports = {
], ],
], ],
}; };
module.exports = config;

View file

@ -1,9 +1,11 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github'); const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula'); const darkCodeTheme = require('prism-react-renderer/themes/dracula');
// With JSDoc @type annotations, IDEs can provide config autocompletion /** @type {import('@docusaurus/types').Config} */
/** @type {import('@docusaurus/types').DocusaurusConfig} */ const config = {
(module.exports = {
title: 'My Site', title: 'My Site',
tagline: 'Dinosaurs are cool', tagline: 'Dinosaurs are cool',
url: 'https://your-docusaurus-test-site.com', url: 'https://your-docusaurus-test-site.com',
@ -111,4 +113,6 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
darkTheme: darkCodeTheme, darkTheme: darkCodeTheme,
}, },
}), }),
}); };
module.exports = config;

View file

@ -6,10 +6,11 @@
* *
* @format * @format
*/ */
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
// With JSDoc @type annotations, IDEs can provide config autocompletion /** @type {import('@docusaurus/types').Config} */
/** @type {import('@docusaurus/types').DocusaurusConfig} */ const config = {
(module.exports = {
title: 'My Site', title: 'My Site',
tagline: 'The tagline of my site', tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com', url: 'https://your-docusaurus-test-site.com',
@ -146,4 +147,6 @@
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
}, },
}), }),
}); };
module.exports = config;

View file

@ -11,7 +11,7 @@ declare module '@docusaurus/mdx-loader' {
[Function, Record<string, unknown>] | Function; [Function, Record<string, unknown>] | Function;
export type RemarkAndRehypePluginOptions = { export type RemarkAndRehypePluginOptions = {
remarkPlugins: RemarkOrRehypePlugin[]; remarkPlugins: RemarkOrRehypePlugin[];
rehypePlugins: string[]; rehypePlugins: RemarkOrRehypePlugin[];
beforeDefaultRemarkPlugins: RemarkOrRehypePlugin[]; beforeDefaultRemarkPlugins: RemarkOrRehypePlugin[];
beforeDefaultRehypePlugins: RemarkOrRehypePlugin[]; beforeDefaultRehypePlugins: RemarkOrRehypePlugin[];
}; };

View file

@ -34,6 +34,7 @@
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"remark-admonitions": "^1.2.1", "remark-admonitions": "^1.2.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"utility-types": "^3.10.0",
"webpack": "^5.40.0" "webpack": "^5.40.0"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -6,7 +6,7 @@
*/ */
declare module '@docusaurus/plugin-content-blog' { declare module '@docusaurus/plugin-content-blog' {
export type Options = import('./types').PluginOptions; export type Options = Partial<import('./types').UserPluginOptions>;
} }
declare module '@theme/BlogSidebar' { declare module '@theme/BlogSidebar' {

View file

@ -11,6 +11,7 @@ import type {
BrokenMarkdownLink, BrokenMarkdownLink,
ContentPaths, ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks'; } from '@docusaurus/utils/lib/markdownLinks';
import {Overwrite} from 'utility-types';
export type BlogContentPaths = ContentPaths; export type BlogContentPaths = ContentPaths;
@ -24,6 +25,20 @@ export interface BlogContent {
export type FeedType = 'rss' | 'atom'; export type FeedType = 'rss' | 'atom';
export type FeedOptions = {
type?: FeedType[] | null;
title?: string;
description?: string;
copyright: string;
language?: string;
};
// Feed options, as provided by user config
export type UserFeedOptions = Overwrite<
Partial<FeedOptions>,
{type?: FeedOptions['type'] | 'all'} // Handle the type: "all" shortcut
>;
export type EditUrlFunction = (editUrlParams: { export type EditUrlFunction = (editUrlParams: {
blogDirPath: string; blogDirPath: string;
blogPath: string; blogPath: string;
@ -63,6 +78,12 @@ export type PluginOptions = RemarkAndRehypePluginOptions & {
authorsMapPath: string; authorsMapPath: string;
}; };
// Options, as provided in the user config (before normalization)
export type UserPluginOptions = Overwrite<
Partial<PluginOptions>,
{feedOptions?: UserFeedOptions}
>;
export interface BlogTags { export interface BlogTags {
[key: string]: BlogTag; [key: string]: BlogTag;
} }

View file

@ -6,7 +6,7 @@
*/ */
declare module '@docusaurus/plugin-content-docs' { declare module '@docusaurus/plugin-content-docs' {
export type Options = import('./types').PluginOptions; export type Options = Partial<import('./types').PluginOptions>;
} }
// TODO public api surface types should rather be exposed as "@docusaurus/plugin-content-docs" // TODO public api surface types should rather be exposed as "@docusaurus/plugin-content-docs"

View file

@ -6,7 +6,7 @@
*/ */
declare module '@docusaurus/plugin-content-pages' { declare module '@docusaurus/plugin-content-pages' {
export type Options = import('./types').PluginOptions; export type Options = Partial<import('./types').PluginOptions>;
} }
declare module '@theme/MDXPage' { declare module '@theme/MDXPage' {

View file

@ -5,4 +5,4 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
export type Options = import('./types').PluginOptions; export type Options = Partial<import('./types').PluginOptions>;

View file

@ -14,10 +14,11 @@ export type Options = {
theme?: import('@docusaurus/theme-classic').Options; theme?: import('@docusaurus/theme-classic').Options;
}; };
export type ThemeConfig = import('@docusaurus/theme-common').ThemeConfig & export type ThemeConfig = import('@docusaurus/types').ThemeConfig &
import('@docusaurus/theme-common').UserThemeConfig &
// Those plugins themeConfigs should rather be moved to preset/plugin options
// Plugin data can be made available to browser thank to the globalData api
import('@docusaurus/plugin-google-analytics').ThemeConfig & import('@docusaurus/plugin-google-analytics').ThemeConfig &
import('@docusaurus/plugin-google-gtag').ThemeConfig & { import('@docusaurus/plugin-google-gtag').ThemeConfig & {
// Those themeConfigs should rather be moved to preset/plugin options
// Plugin data can be made available to browser thank to the globalData api
algolia?: unknown; // TODO type plugin algolia?: unknown; // TODO type plugin
}; };

View file

@ -92,7 +92,7 @@ function getInfimaCSSFile(direction: string) {
} }
export type PluginOptions = { export type PluginOptions = {
customCss?: string; customCss?: string | string[];
}; };
export default function docusaurusThemeClassic( export default function docusaurusThemeClassic(

View file

@ -11,7 +11,7 @@
/// <reference types="@docusaurus/plugin-content-pages" /> /// <reference types="@docusaurus/plugin-content-pages" />
declare module '@docusaurus/theme-classic' { declare module '@docusaurus/theme-classic' {
export type Options = import('./index').PluginOptions; export type Options = Partial<import('./index').PluginOptions>;
} }
declare module '@theme/AnnouncementBar' { declare module '@theme/AnnouncementBar' {

View file

@ -25,7 +25,8 @@
"@docusaurus/types": "2.0.0-beta.6", "@docusaurus/types": "2.0.0-beta.6",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"tslib": "^2.3.1" "tslib": "^2.3.1",
"utility-types": "^3.10.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.6", "@docusaurus/module-type-aliases": "2.0.0-beta.6",

View file

@ -9,6 +9,7 @@ export {useThemeConfig} from './utils/useThemeConfig';
export type { export type {
ThemeConfig, ThemeConfig,
UserThemeConfig,
Navbar, Navbar,
NavbarItem, NavbarItem,
NavbarLogo, NavbarLogo,

View file

@ -7,6 +7,7 @@
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {PrismTheme} from 'prism-react-renderer'; import {PrismTheme} from 'prism-react-renderer';
import {CSSProperties} from 'react'; import {CSSProperties} from 'react';
import {DeepPartial} from 'utility-types';
export type DocsVersionPersistence = 'localStorage' | 'none'; export type DocsVersionPersistence = 'localStorage' | 'none';
@ -16,7 +17,7 @@ export type NavbarItem = {
items?: NavbarItem[]; items?: NavbarItem[];
label?: string; label?: string;
position?: 'left' | 'right'; position?: 'left' | 'right';
}; } & Record<string, unknown>;
export type NavbarLogo = { export type NavbarLogo = {
src: string; src: string;
@ -90,6 +91,7 @@ export type TableOfContents = {
maxHeadingLevel: number; maxHeadingLevel: number;
}; };
// Theme config after validation/normalization
export type ThemeConfig = { export type ThemeConfig = {
docs: { docs: {
versionPersistence: DocsVersionPersistence; versionPersistence: DocsVersionPersistence;
@ -112,6 +114,9 @@ export type ThemeConfig = {
tableOfContents: TableOfContents; tableOfContents: TableOfContents;
}; };
// User-provided theme config, unnormalized
export type UserThemeConfig = DeepPartial<ThemeConfig>;
export function useThemeConfig(): ThemeConfig { export function useThemeConfig(): ThemeConfig {
return useDocusaurusContext().siteConfig.themeConfig as ThemeConfig; return useDocusaurusContext().siteConfig.themeConfig as ThemeConfig;
} }

View file

@ -19,6 +19,7 @@
"commander": "^5.1.0", "commander": "^5.1.0",
"joi": "^17.4.2", "joi": "^17.4.2",
"querystring": "0.2.0", "querystring": "0.2.0",
"utility-types": "^3.10.0",
"webpack": "^5.40.0", "webpack": "^5.40.0",
"webpack-merge": "^5.8.0" "webpack-merge": "^5.8.0"
} }

View file

@ -9,6 +9,7 @@ import type {RuleSetRule, Configuration} from 'webpack';
import type {Command} from 'commander'; import type {Command} from 'commander';
import type {ParsedUrlQueryInput} from 'querystring'; import type {ParsedUrlQueryInput} from 'querystring';
import type Joi from 'joi'; import type Joi from 'joi';
import type {Overwrite, DeepPartial} from 'utility-types';
// Convert webpack-merge webpack-merge enum to union type // Convert webpack-merge webpack-merge enum to union type
// For type retro-compatible webpack-merge upgrade: we used string literals before) // For type retro-compatible webpack-merge upgrade: we used string literals before)
@ -21,6 +22,7 @@ export type ThemeConfig = {
[key: string]: unknown; [key: string]: unknown;
}; };
// Docusaurus config, after validation/normalization
export interface DocusaurusConfig { export interface DocusaurusConfig {
baseUrl: string; baseUrl: string;
baseUrlIssueBanner: boolean; baseUrlIssueBanner: boolean;
@ -68,6 +70,19 @@ export interface DocusaurusConfig {
}; };
} }
// Docusaurus config, as provided by the user (partial/unnormalized)
// This type is used to provide type-safety / IDE auto-complete on the config file
// See https://docusaurus.io/docs/typescript-support
export type Config = Overwrite<
Partial<DocusaurusConfig>,
{
title: Required<DocusaurusConfig['title']>;
url: Required<DocusaurusConfig['url']>;
baseUrl: Required<DocusaurusConfig['baseUrl']>;
i18n?: DeepPartial<DocusaurusConfig['i18n']>;
}
>;
/** /**
* - `type: 'package'`, plugin is in a different package. * - `type: 'package'`, plugin is in a different package.
* - `type: 'project'`, plugin is in the same docusaurus project. * - `type: 'project'`, plugin is in the same docusaurus project.

View file

@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path');
exports.dogfoodingPluginInstances = [ /** @type {import('@docusaurus/types').PluginConfig[]} */
const dogfoodingPluginInstances = [
[ [
'@docusaurus/plugin-content-docs', '@docusaurus/plugin-content-docs',
/** @type {import('@docusaurus/plugin-content-docs').Options} */ /** @type {import('@docusaurus/plugin-content-docs').Options} */
@ -43,3 +43,5 @@ exports.dogfoodingPluginInstances = [
}), }),
], ],
]; ];
exports.dogfoodingPluginInstances = dogfoodingPluginInstances;

View file

@ -42,8 +42,11 @@ It is **not possible** to use a TypeScript config file in Docusaurus, unless you
We recommend using [JSDoc type annotations](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html): We recommend using [JSDoc type annotations](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html):
<!-- prettier-ignore-start -->
```js title="docusaurus.config.js" ```js title="docusaurus.config.js"
// highlight-start
// @ts-check
// highlight-end
// highlight-start // highlight-start
/** @type {import('@docusaurus/types').Plugin} */ /** @type {import('@docusaurus/types').Plugin} */
// highlight-end // highlight-end
@ -54,9 +57,9 @@ function MyPlugin(context, options) {
} }
// highlight-start // highlight-start
/** @type {import('@docusaurus/types').DocusaurusConfig} */ /** @type {import('@docusaurus/types').Config} */
// highlight-end // highlight-end
(module.exports = { const config = {
title: 'Docusaurus', title: 'Docusaurus',
tagline: 'Build optimized websites quickly, focus on your content', tagline: 'Build optimized websites quickly, focus on your content',
organizationName: 'facebook', organizationName: 'facebook',
@ -98,9 +101,10 @@ function MyPlugin(context, options) {
}, },
}, },
}), }),
}); };
module.exports = config;
``` ```
<!-- prettier-ignore-end -->
:::tip :::tip
@ -110,6 +114,18 @@ The best IDEs (VSCode, WebStorm, Intellij...) will provide a nice auto-completio
::: :::
:::info
By default, the Docusaurus TypeScript config does not type-check JavaScript files.
The `// @ts-check` comment ensures the config file is properly type-checked when running:
```bash npm2yarn
npm run tsc
```
:::
## Swizzling TypeScript theme components {#swizzling-typescript-theme-components} ## Swizzling TypeScript theme components {#swizzling-typescript-theme-components}
For themes that supports TypeScript theme components, you can add the `--typescript` flag to the end of swizzling command to get TypeScript source code. For example, the following command will generate `index.tsx` and `styles.module.css` into `src/theme/Footer`. For themes that supports TypeScript theme components, you can add the `--typescript` flag to the end of swizzling command to get TypeScript source code. For example, the following command will generate `index.tsx` and `styles.module.css` into `src/theme/Footer`.

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.
*/ */
// @ts-check
const path = require('path'); const path = require('path');
const versions = require('./versions.json'); const versions = require('./versions.json');
@ -49,8 +50,8 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging;
const TwitterSvg = const TwitterSvg =
'<svg style="fill: #1DA1F2; vertical-align: middle;" width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>'; '<svg style="fill: #1DA1F2; vertical-align: middle;" width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>';
/** @type {import('@docusaurus/types').DocusaurusConfig} */ /** @type {import('@docusaurus/types').Config} */
(module.exports = { const config = {
title: 'Docusaurus', title: 'Docusaurus',
tagline: 'Build optimized websites quickly, focus on your content', tagline: 'Build optimized websites quickly, focus on your content',
organizationName: 'facebook', organizationName: 'facebook',
@ -486,4 +487,6 @@ const TwitterSvg =
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
}, },
}), }),
}); };
module.exports = config;

View file

@ -24,7 +24,8 @@
"netlify:crowdin:downloadTranslationsFailSafe": "yarn netlify:crowdin:wait && (yarn --cwd .. crowdin:download:website || echo 'Crowdin translation download failure (only internal PRs have access to the Crowdin env token)')", "netlify:crowdin:downloadTranslationsFailSafe": "yarn netlify:crowdin:wait && (yarn --cwd .. crowdin:download:website || echo 'Crowdin translation download failure (only internal PRs have access to the Crowdin env token)')",
"netlify:crowdin:uploadSources": "yarn --cwd .. crowdin:upload:website", "netlify:crowdin:uploadSources": "yarn --cwd .. crowdin:upload:website",
"netlify:test": "yarn netlify:build:deployPreview && yarn netlify dev --debug", "netlify:test": "yarn netlify:build:deployPreview && yarn netlify dev --debug",
"typecheck": "tsc" "typecheck": "tsc",
"watch": "tsc --watch"
}, },
"dependencies": { "dependencies": {
"@crowdin/cli": "^3.5.2", "@crowdin/cli": "^3.5.2",

View file

@ -9,7 +9,7 @@
function FeatureRequestsPlugin() { function FeatureRequestsPlugin() {
return { return {
name: 'feature-requests-plugin', name: 'feature-requests-plugin',
contentLoaded({actions}) { async contentLoaded({actions}) {
actions.addRoute({ actions.addRoute({
path: '/feature-requests', path: '/feature-requests',
exact: false, exact: false,