feat: TypeScript presets/plugins should expose Options typing (#5456)

* each TS plugin should export option types + preset export option / themeConfig types + remove TS typechecking for the bootstrap theme

* each TS plugin should export option types + preset export option / themeConfig types + remove TS typechecking for the bootstrap theme

* fix remaining TS errors

* fix remaining TS errors

* TS fix

* Add JSDoc type annotations to init templates and TS docs

* missing title char
This commit is contained in:
Sébastien Lorber 2021-09-01 12:14:40 +02:00 committed by GitHub
parent 013cfc07bb
commit 553f914639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 514 additions and 403 deletions

View file

@ -1,8 +1,9 @@
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').DocusaurusConfig} */ /** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = { (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',
@ -12,7 +13,33 @@ module.exports = {
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
organizationName: 'facebook', // Usually your GitHub org/user name. organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name. projectName: 'docusaurus', // Usually your repo name.
themeConfig: {
presets: [
[
'@docusaurus/preset-classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: { navbar: {
title: 'My Site', title: 'My Site',
logo: { logo: {
@ -83,26 +110,5 @@ module.exports = {
theme: lightCodeTheme, theme: lightCodeTheme,
darkTheme: darkCodeTheme, darkTheme: darkCodeTheme,
}, },
}, }),
presets: [ });
[
'@docusaurus/preset-classic',
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
},
],
],
};

View file

@ -7,8 +7,9 @@
* @format * @format
*/ */
// With JSDoc @type annotations, IDEs can provide config autocompletion
/** @type {import('@docusaurus/types').DocusaurusConfig} */ /** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = { (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',
@ -18,7 +19,33 @@ module.exports = {
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
organizationName: 'facebook', // Usually your GitHub org/user name. organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name. projectName: 'docusaurus', // Usually your repo name.
themeConfig: {
presets: [
[
'@docusaurus/preset-classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: { navbar: {
title: 'My Facebook Project', title: 'My Facebook Project',
logo: { logo: {
@ -118,26 +145,5 @@ module.exports = {
// Please do not remove the credits, help to publicize Docusaurus :) // Please do not remove the credits, help to publicize Docusaurus :)
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
}, },
}, }),
presets: [ });
[
'@docusaurus/preset-classic',
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
},
],
],
};

View file

@ -3,6 +3,7 @@
"version": "2.0.0-beta.5", "version": "2.0.0-beta.5",
"description": "Client redirects plugin for Docusaurus.", "description": "Client redirects plugin for Docusaurus.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "src/plugin-client-redirects.d.ts",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"watch": "tsc --watch" "watch": "tsc --watch"

View file

@ -0,0 +1,8 @@
/**
* 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 type Options = import('./types').UserPluginOptions;

View file

@ -3,7 +3,7 @@
"version": "2.0.0-beta.5", "version": "2.0.0-beta.5",
"description": "Blog plugin for Docusaurus.", "description": "Blog plugin for Docusaurus.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "index.d.ts", "types": "src/plugin-content-blog.d.ts",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"watch": "tsc --watch" "watch": "tsc --watch"

View file

@ -5,6 +5,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
declare module '@docusaurus/plugin-content-blog' {
export type Options = import('./types').PluginOptions;
}
declare module '@theme/BlogSidebar' { declare module '@theme/BlogSidebar' {
export type BlogSidebarItem = {title: string; permalink: string}; export type BlogSidebarItem = {title: string; permalink: string};
export type BlogSidebar = { export type BlogSidebar = {
@ -24,8 +28,8 @@ declare module '@theme/BlogPostPage' {
import type {BlogSidebar} from '@theme/BlogSidebar'; import type {BlogSidebar} from '@theme/BlogSidebar';
import type {TOCItem} from '@docusaurus/types'; import type {TOCItem} from '@docusaurus/types';
export type FrontMatter = import('./src/blogFrontMatter').BlogPostFrontMatter; export type FrontMatter = import('./blogFrontMatter').BlogPostFrontMatter;
export type Assets = import('./src/types').Assets; export type Assets = import('./types').Assets;
export type Metadata = { export type Metadata = {
readonly title: string; readonly title: string;
@ -38,7 +42,7 @@ declare module '@theme/BlogPostPage' {
readonly truncated?: string; readonly truncated?: string;
readonly nextItem?: {readonly title: string; readonly permalink: string}; readonly nextItem?: {readonly title: string; readonly permalink: string};
readonly prevItem?: {readonly title: string; readonly permalink: string}; readonly prevItem?: {readonly title: string; readonly permalink: string};
readonly authors: import('./src/types').Author[]; readonly authors: import('./types').Author[];
readonly tags: readonly { readonly tags: readonly {
readonly label: string; readonly label: string;
readonly permalink: string; readonly permalink: string;
@ -122,4 +126,7 @@ declare module '@theme/BlogTagsPostsPage' {
readonly metadata: Tag; readonly metadata: Tag;
readonly items: readonly {readonly content: Content}[]; readonly items: readonly {readonly content: Content}[];
}; };
const BlogTagsPostsPage: (props: Props) => JSX.Element;
export default BlogTagsPostsPage;
} }

View file

@ -5,6 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
declare module '@docusaurus/plugin-content-docs' {
export type Options = import('./types').PluginOptions;
}
// TODO public api surface types should rather be exposed as "@docusaurus/plugin-content-docs"
declare module '@docusaurus/plugin-content-docs-types' { declare module '@docusaurus/plugin-content-docs-types' {
type VersionBanner = import('./types').VersionBanner; type VersionBanner = import('./types').VersionBanner;
type GlobalDataVersion = import('./types').GlobalVersion; type GlobalDataVersion = import('./types').GlobalVersion;

View file

@ -5,6 +5,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
declare module '@docusaurus/plugin-content-pages' {
export type Options = import('./types').PluginOptions;
}
declare module '@theme/MDXPage' { declare module '@theme/MDXPage' {
import type {TOCItem} from '@docusaurus/types'; import type {TOCItem} from '@docusaurus/types';

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
// TODO legacy, this does not seem like a good place to add those TS types!
declare module 'remark-admonitions' { declare module 'remark-admonitions' {
type Options = Record<string, unknown>; type Options = Record<string, unknown>;

View file

@ -3,6 +3,7 @@
"version": "2.0.0-beta.5", "version": "2.0.0-beta.5",
"description": "Simple sitemap generation plugin for Docusaurus.", "description": "Simple sitemap generation plugin for Docusaurus.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "src/plugin-sitemap.d.ts",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"watch": "tsc --watch" "watch": "tsc --watch"

View file

@ -0,0 +1,8 @@
/**
* 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 type Options = import('./types').PluginOptions;

View file

@ -3,6 +3,7 @@
"version": "2.0.0-beta.5", "version": "2.0.0-beta.5",
"description": "Classic preset for Docusaurus.", "description": "Classic preset for Docusaurus.",
"main": "src/index.js", "main": "src/index.js",
"types": "src/preset-classic.d.ts",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },

View file

@ -6,7 +6,7 @@
*/ */
module.exports = function preset(context, opts = {}) { module.exports = function preset(context, opts = {}) {
const {siteConfig = {}} = context; const {siteConfig} = context;
const {themeConfig} = siteConfig; const {themeConfig} = siteConfig;
const {algolia, googleAnalytics, gtag} = themeConfig; const {algolia, googleAnalytics, gtag} = themeConfig;
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';

View file

@ -0,0 +1,23 @@
/**
* 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 type Options = {
debug?: boolean;
docs?: false | import('@docusaurus/plugin-content-docs').Options;
blog?: false | import('@docusaurus/plugin-content-blog').Options;
pages?: false | import('@docusaurus/plugin-content-pages').Options;
sitemap?: false | import('@docusaurus/plugin-sitemap').Options;
theme?: import('@docusaurus/theme-classic').Options;
};
export type ThemeConfig = import('@docusaurus/theme-common').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
googleAnalytics?: unknown; // TODO type plugin
gtag?: unknown; // TODO type plugin
};

View file

@ -31,7 +31,7 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.5" "@docusaurus/module-type-aliases": "2.0.0-beta.5"
}, },
"scripts": { "scripts": {
"build": "tsc --noEmit && yarn babel && yarn prettier", "build": "yarn babel && yarn prettier",
"watch": "yarn babel --watch", "watch": "yarn babel --watch",
"babel": "babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files", "babel": "babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
"prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --write \"**/*.{js,ts}\"" "prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --write \"**/*.{js,ts}\""

View file

@ -89,7 +89,7 @@ function getInfimaCSSFile(direction) {
}.css`; }.css`;
} }
type PluginOptions = { export type PluginOptions = {
customCss?: string; customCss?: string;
}; };

View file

@ -32,7 +32,7 @@ function useBlogPostsPlural() {
); );
} }
function BlogTagsPostPage(props: Props): JSX.Element { export default function BlogTagsPostsPage(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar} = props;
const {allTagsPath, name: tagName, count} = metadata; const {allTagsPath, name: tagName, count} = metadata;
const blogPostsPlural = useBlogPostsPlural(); const blogPostsPlural = useBlogPostsPlural();
@ -80,5 +80,3 @@ function BlogTagsPostPage(props: Props): JSX.Element {
</BlogLayout> </BlogLayout>
); );
} }
export default BlogTagsPostPage;

View file

@ -51,7 +51,7 @@ const MDXComponents: MDXComponentsObject = {
// See comment for `code` above // See comment for `code` above
if (isValidElement(children) && isValidElement(children?.props?.children)) { if (isValidElement(children) && isValidElement(children?.props?.children)) {
return children?.props.children; return children.props.children;
} }
return ( return (

View file

@ -12,6 +12,10 @@
/// <reference types="@docusaurus/plugin-content-docs" /> /// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/plugin-content-pages" /> /// <reference types="@docusaurus/plugin-content-pages" />
declare module '@docusaurus/theme-classic' {
export type Options = import('./index').PluginOptions;
}
declare module '@theme/AnnouncementBar' { declare module '@theme/AnnouncementBar' {
const AnnouncementBar: () => JSX.Element | null; const AnnouncementBar: () => JSX.Element | null;
export default AnnouncementBar; export default AnnouncementBar;

View file

@ -4,7 +4,8 @@ const path = require('path');
exports.dogfoodingPluginInstances = [ exports.dogfoodingPluginInstances = [
[ [
'@docusaurus/plugin-content-docs', '@docusaurus/plugin-content-docs',
{ /** @type {import('@docusaurus/plugin-content-docs').Options} */
({
id: 'docs-tests', id: 'docs-tests',
routeBasePath: '/tests/docs', routeBasePath: '/tests/docs',
sidebarPath: '_dogfooding/docs-tests-sidebars.js', sidebarPath: '_dogfooding/docs-tests-sidebars.js',
@ -12,12 +13,13 @@ exports.dogfoodingPluginInstances = [
// Using a symlinked folder as source, test for use-case https://github.com/facebook/docusaurus/issues/3272 // Using a symlinked folder as source, test for use-case https://github.com/facebook/docusaurus/issues/3272
// The target folder uses a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079 // The target folder uses a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079
path: fs.realpathSync('_dogfooding/docs-tests-symlink'), path: fs.realpathSync('_dogfooding/docs-tests-symlink'),
}, }),
], ],
[ [
'@docusaurus/plugin-content-blog', '@docusaurus/plugin-content-blog',
{ /** @type {import('@docusaurus/plugin-content-blog').Options} */
({
id: 'blog-tests', id: 'blog-tests',
path: '_dogfooding/_blog tests', path: '_dogfooding/_blog tests',
routeBasePath: '/tests/blog', routeBasePath: '/tests/blog',
@ -28,15 +30,16 @@ exports.dogfoodingPluginInstances = [
type: 'all', type: 'all',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
}, },
}, }),
], ],
[ [
'@docusaurus/plugin-content-pages', '@docusaurus/plugin-content-pages',
{ /** @type {import('@docusaurus/plugin-content-pages').Options} */
({
id: 'pages-tests', id: 'pages-tests',
path: '_dogfooding/_pages tests', path: '_dogfooding/_pages tests',
routeBasePath: '/tests/pages', routeBasePath: '/tests/pages',
}, }),
], ],
]; ];

View file

@ -42,7 +42,7 @@ 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):
```js title="docusaurus.config.js ```js title="docusaurus.config.js"
// highlight-start // highlight-start
/** @type {import('@docusaurus/types').Plugin} */ /** @type {import('@docusaurus/types').Plugin} */
// highlight-end // highlight-end
@ -55,15 +55,49 @@ function MyPlugin(context, options) {
// highlight-start // highlight-start
/** @type {import('@docusaurus/types').DocusaurusConfig} */ /** @type {import('@docusaurus/types').DocusaurusConfig} */
// highlight-end // highlight-end
const config = { (module.exports = {
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',
projectName: 'docusaurus', projectName: 'docusaurus',
plugins: [MyPlugin], plugins: [MyPlugin],
}; presets: [
[
module.exports = config; '@docusaurus/preset-classic',
// highlight-start
/** @type {import('@docusaurus/preset-classic').Options} */
// highlight-end
({
docs: {
path: 'docs',
sidebarPath: 'sidebars.js',
},
blog: {
path: 'blog',
postsPerPage: 5,
},
}),
],
],
themeConfig:
// highlight-start
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
// highlight-end
({
colorMode: {
defaultMode: 'dark',
},
navbar: {
hideOnScroll: true,
title: 'Docusaurus',
logo: {
alt: 'Docusaurus Logo',
src: 'img/docusaurus.svg',
srcDark: 'img/docusaurus_keytar.svg',
},
},
}),
});
``` ```
:::tip :::tip

View file

@ -38,7 +38,6 @@ const isDeployPreview =
process.env.NETLIFY && process.env.CONTEXT === 'deploy-preview'; process.env.NETLIFY && process.env.CONTEXT === 'deploy-preview';
const baseUrl = process.env.BASE_URL || '/'; const baseUrl = process.env.BASE_URL || '/';
const isBootstrapPreset = process.env.DOCUSAURUS_PRESET === 'bootstrap';
// Special deployment for staging locales until they get enough translations // Special deployment for staging locales until they get enough translations
// https://app.netlify.com/sites/docusaurus-i18n-staging // https://app.netlify.com/sites/docusaurus-i18n-staging
@ -105,7 +104,8 @@ const TwitterSvg =
require('./src/featureRequests/FeatureRequestsPlugin'), require('./src/featureRequests/FeatureRequestsPlugin'),
[ [
'@docusaurus/plugin-content-docs', '@docusaurus/plugin-content-docs',
{ /** @type {import('@docusaurus/plugin-content-docs').Options} */
({
id: 'community', id: 'community',
path: 'community', path: 'community',
routeBasePath: 'community', routeBasePath: 'community',
@ -119,11 +119,12 @@ const TwitterSvg =
sidebarPath: require.resolve('./sidebarsCommunity.js'), sidebarPath: require.resolve('./sidebarsCommunity.js'),
showLastUpdateAuthor: true, showLastUpdateAuthor: true,
showLastUpdateTime: true, showLastUpdateTime: true,
}, }),
], ],
[ [
'@docusaurus/plugin-client-redirects', '@docusaurus/plugin-client-redirects',
{ /** @type {import('@docusaurus/plugin-client-redirects').Options} */
({
fromExtensions: ['html'], fromExtensions: ['html'],
createRedirects: function (path) { createRedirects: function (path) {
// redirect to /docs from /docs/introduction, // redirect to /docs from /docs/introduction,
@ -146,7 +147,7 @@ const TwitterSvg =
to: '/community/resources', to: '/community/resources',
}, },
], ],
}, }),
], ],
[ [
'@docusaurus/plugin-ideal-image', '@docusaurus/plugin-ideal-image',
@ -222,10 +223,9 @@ const TwitterSvg =
], ],
presets: [ presets: [
[ [
isBootstrapPreset '@docusaurus/preset-classic',
? '@docusaurus/preset-bootstrap' /** @type {import('@docusaurus/preset-classic').Options} */
: '@docusaurus/preset-classic', ({
{
debug: true, // force debug plugin usage debug: true, // force debug plugin usage
docs: { docs: {
// routeBasePath: '/', // routeBasePath: '/',
@ -284,10 +284,13 @@ const TwitterSvg =
theme: { theme: {
customCss: [require.resolve('./src/css/custom.css')], customCss: [require.resolve('./src/css/custom.css')],
}, },
}, }),
], ],
], ],
themeConfig: {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
liveCodeBlock: { liveCodeBlock: {
playgroundPosition: 'bottom', playgroundPosition: 'bottom',
}, },
@ -482,5 +485,5 @@ const TwitterSvg =
}, },
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
}, },
}, }),
}); });

View file

@ -14,8 +14,6 @@
"write-heading-ids": "docusaurus write-heading-ids", "write-heading-ids": "docusaurus write-heading-ids",
"start:baseUrl": "cross-env BASE_URL='/build/' yarn start", "start:baseUrl": "cross-env BASE_URL='/build/' yarn start",
"build:baseUrl": "cross-env BASE_URL='/build/' yarn build", "build:baseUrl": "cross-env BASE_URL='/build/' yarn build",
"start:bootstrap": "cross-env DOCUSAURUS_PRESET=bootstrap yarn start",
"build:bootstrap": "cross-env DOCUSAURUS_PRESET=bootstrap yarn build",
"start:blogOnly": "cross-env yarn start --config=docusaurus.config-blog-only.js", "start:blogOnly": "cross-env yarn start --config=docusaurus.config-blog-only.js",
"build:blogOnly": "cross-env yarn build --config=docusaurus.config-blog-only.js", "build:blogOnly": "cross-env yarn build --config=docusaurus.config-blog-only.js",
"netlify:build:production": "yarn docusaurus write-translations && yarn netlify:crowdin:delay && yarn netlify:crowdin:uploadSources && yarn netlify:crowdin:downloadTranslations && yarn build", "netlify:build:production": "yarn docusaurus write-translations && yarn netlify:crowdin:delay && yarn netlify:crowdin:uploadSources && yarn netlify:crowdin:downloadTranslations && yarn build",