chore: clean up ESLint config, enable a few rules (#6514)

* chore: clean up ESLint config, enable a few rules

* enable max-len for comments

* fix build
This commit is contained in:
Joshua Chen 2022-01-31 10:31:24 +08:00 committed by GitHub
parent b8ccb869f1
commit aa446b7a9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
167 changed files with 1157 additions and 960 deletions

View file

@ -22,15 +22,14 @@ module.exports = {
allowImportExportEverywhere: true, allowImportExportEverywhere: true,
}, },
globals: { globals: {
testStylelintRule: true, JSX: true,
}, },
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'plugin:jest/recommended', 'plugin:jest/recommended',
'airbnb', 'airbnb',
'plugin:@typescript-eslint/recommended',
'prettier', 'prettier',
], ],
settings: { settings: {
@ -41,111 +40,37 @@ module.exports = {
}, },
}, },
reportUnusedDisableDirectives: true, reportUnusedDisableDirectives: true,
plugins: ['react-hooks', 'header', 'jest'], plugins: ['react-hooks', 'header', 'jest', '@typescript-eslint'],
rules: { rules: {
'react-hooks/rules-of-hooks': ERROR,
'react-hooks/exhaustive-deps': ERROR,
'class-methods-use-this': OFF, // It's a way of allowing private variables.
'func-names': OFF,
// Ignore certain webpack alias because it can't be resolved
'import/no-unresolved': [
ERROR,
{
ignore: ['^@theme', '^@docusaurus', '^@generated', '^@site'],
},
],
'import/extensions': OFF,
'no-restricted-exports': OFF,
'header/header': [
ERROR,
'block',
[
'*',
' * 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.',
' ',
],
],
'jsx-a11y/click-events-have-key-events': WARNING,
'jsx-a11y/no-noninteractive-element-interactions': WARNING,
'jsx-a11y/html-has-lang': OFF,
'no-console': OFF,
'no-else-return': OFF,
'no-param-reassign': [WARNING, {props: false}],
'no-underscore-dangle': OFF,
curly: [WARNING, 'all'],
'react/jsx-filename-extension': OFF,
'react/no-array-index-key': OFF, // Sometimes its ok, e.g. non-changing data.
'react/prop-types': OFF,
'react/destructuring-assignment': OFF, // Too many lines.
'react/prefer-stateless-function': WARNING,
'react/jsx-props-no-spreading': OFF,
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
'react/function-component-definition': [
WARNING,
{
namedComponents: 'function-declaration',
unnamedComponents: 'arrow-function',
},
],
'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}],
'@typescript-eslint/no-inferrable-types': OFF,
'@typescript-eslint/consistent-type-imports': [
WARNING,
{disallowTypeAnnotations: false},
],
'import/order': OFF,
'import/prefer-default-export': OFF,
'lines-between-class-members': OFF,
'no-lonely-if': WARNING,
'no-use-before-define': OFF,
'@typescript-eslint/no-use-before-define': [
ERROR,
{functions: false, classes: false, variables: true},
],
'no-unused-vars': OFF,
'no-nested-ternary': WARNING,
'@typescript-eslint/no-empty-function': OFF,
'@typescript-eslint/no-non-null-assertion': OFF,
'@typescript-eslint/no-unused-vars': [
ERROR,
{argsIgnorePattern: '^_', ignoreRestSiblings: true},
],
'@typescript-eslint/explicit-module-boundary-types': WARNING,
'@typescript-eslint/ban-ts-comment': [
ERROR,
{'ts-expect-error': 'allow-with-description'},
],
'import/no-extraneous-dependencies': ERROR,
'no-useless-escape': WARNING,
'prefer-template': WARNING,
'no-template-curly-in-string': WARNING,
'array-callback-return': WARNING, 'array-callback-return': WARNING,
camelcase: WARNING, camelcase: WARNING,
'no-restricted-syntax': WARNING, 'class-methods-use-this': OFF, // It's a way of allowing private variables.
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}], curly: [WARNING, 'all'],
'global-require': WARNING, 'global-require': WARNING,
'prefer-destructuring': WARNING, 'lines-between-class-members': OFF,
yoda: WARNING, 'max-len': [
'no-await-in-loop': OFF, WARNING,
'no-control-regex': WARNING,
'no-empty': [WARNING, {allowEmptyCatch: true}],
'no-prototype-builtins': WARNING,
'no-case-declarations': WARNING,
'no-undef': OFF,
'no-shadow': OFF,
'@typescript-eslint/no-shadow': ERROR,
'no-redeclare': OFF,
'@typescript-eslint/no-redeclare': ERROR,
'@typescript-eslint/no-empty-interface': [
ERROR,
{ {
allowSingleExtends: true, code: Infinity, // Code width is already enforced by Prettier
tabWidth: 2,
comments: 80,
ignoreUrls: true,
ignorePattern: '(eslint-disable|@)',
}, },
], ],
'@typescript-eslint/method-signature-style': ERROR, 'no-await-in-loop': OFF,
'no-case-declarations': WARNING,
'no-console': OFF,
'no-control-regex': WARNING,
'no-else-return': [WARNING, {allowElseIf: true}],
'no-empty': [WARNING, {allowEmptyCatch: true}],
'no-lonely-if': WARNING,
'no-nested-ternary': WARNING,
'no-param-reassign': [WARNING, {props: false}],
'no-prototype-builtins': WARNING,
'no-restricted-exports': OFF,
'no-useless-escape': WARNING,
'no-template-curly-in-string': WARNING,
'no-restricted-imports': [ 'no-restricted-imports': [
ERROR, ERROR,
{ {
@ -153,7 +78,9 @@ module.exports = {
{ {
name: 'lodash', name: 'lodash',
importNames: [ importNames: [
// 'compact', // TODO: TS doesn't make Boolean a narrowing function yet, so filter(Boolean) is problematic type-wise // TODO: TS doesn't make Boolean a narrowing function yet,
// so filter(Boolean) is problematic type-wise
// 'compact',
'filter', 'filter',
'flatten', 'flatten',
'flatMap', 'flatMap',
@ -170,16 +97,104 @@ module.exports = {
], ],
}, },
], ],
'no-restricted-syntax': WARNING,
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
'prefer-destructuring': WARNING,
'prefer-template': WARNING,
yoda: WARNING,
'header/header': [
ERROR,
'block',
[
'*',
' * Copyright (c) Facebook, Inc. and its affiliates.',
' *',
' * This source code is licensed under the MIT license found in the',
' * LICENSE file in the root directory of this source tree.',
' ',
],
],
'import/extensions': OFF,
// Ignore certain webpack aliases because they can't be resolved
'import/no-unresolved': [
ERROR,
{
ignore: ['^@theme', '^@docusaurus', '^@generated', '^@site'],
},
],
'import/order': OFF,
'import/prefer-default-export': OFF,
'jest/prefer-expect-resolves': WARNING, 'jest/prefer-expect-resolves': WARNING,
'jest/expect-expect': OFF, 'jest/expect-expect': OFF,
'jest/valid-title': OFF, 'jest/valid-title': OFF,
'jsx-a11y/click-events-have-key-events': WARNING,
'jsx-a11y/no-noninteractive-element-interactions': WARNING,
'jsx-a11y/html-has-lang': OFF,
'react-hooks/rules-of-hooks': ERROR,
'react-hooks/exhaustive-deps': ERROR,
// Sometimes we do need the props as a whole, e.g. when spreading
'react/destructuring-assignment': OFF,
'react/function-component-definition': [
WARNING,
{
namedComponents: 'function-declaration',
unnamedComponents: 'arrow-function',
},
],
'react/jsx-filename-extension': OFF,
'react/jsx-props-no-spreading': OFF,
'react/no-array-index-key': OFF, // We build a static site, and nearly all components don't change.
'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}],
'react/prefer-stateless-function': WARNING,
'react/prop-types': OFF,
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
'@typescript-eslint/ban-ts-comment': [
ERROR,
{'ts-expect-error': 'allow-with-description'},
],
'@typescript-eslint/consistent-type-imports': [
WARNING,
{disallowTypeAnnotations: false},
],
'@typescript-eslint/explicit-module-boundary-types': WARNING,
'@typescript-eslint/method-signature-style': ERROR,
'@typescript-eslint/no-empty-function': OFF,
'@typescript-eslint/no-empty-interface': [
ERROR,
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-inferrable-types': OFF,
'no-use-before-define': OFF,
'@typescript-eslint/no-use-before-define': [
ERROR,
{functions: false, classes: false, variables: true},
],
'@typescript-eslint/no-non-null-assertion': OFF,
'no-redeclare': OFF,
'@typescript-eslint/no-redeclare': ERROR,
'no-shadow': OFF,
'@typescript-eslint/no-shadow': ERROR,
'no-unused-vars': OFF,
'@typescript-eslint/no-unused-vars': [
ERROR,
{argsIgnorePattern: '^_', ignoreRestSiblings: true},
],
}, },
overrides: [ overrides: [
{ {
files: [ files: [
'packages/docusaurus-theme-*/src/theme/**/*.js', 'packages/docusaurus-*/src/theme/**/*.js',
'packages/docusaurus-theme-*/src/theme/**/*.ts', 'packages/docusaurus-*/src/theme/**/*.ts',
'packages/docusaurus-theme-*/src/theme/**/*.tsx', 'packages/docusaurus-*/src/theme/**/*.tsx',
], ],
rules: { rules: {
'import/no-named-export': ERROR, 'import/no-named-export': ERROR,
@ -206,6 +221,7 @@ module.exports = {
{ {
files: ['*.ts', '*.tsx'], files: ['*.ts', '*.tsx'],
rules: { rules: {
'no-undef': OFF,
'import/no-import-module-exports': OFF, 'import/no-import-module-exports': OFF,
}, },
}, },

View file

@ -62,9 +62,11 @@ describe('packages', () => {
.filter((packageJsonFile) => packageJsonFile.content.name.startsWith('@')) .filter((packageJsonFile) => packageJsonFile.content.name.startsWith('@'))
.forEach((packageJsonFile) => { .forEach((packageJsonFile) => {
if (packageJsonFile) { if (packageJsonFile) {
// Unfortunately jest custom message do not exist in loops, so using an exception instead to show failing package file // Unfortunately jest custom message do not exist in loops,
// so using an exception instead to show failing package file
// (see https://github.com/facebook/jest/issues/3293) // (see https://github.com/facebook/jest/issues/3293)
// expect(packageJsonFile.content.publishConfig?.access).toEqual('public'); // expect(packageJsonFile.content.publishConfig?.access)
// .toEqual('public');
if (packageJsonFile.content.publishConfig?.access !== 'public') { if (packageJsonFile.content.publishConfig?.access !== 'public') {
throw new Error( throw new Error(
`Package ${packageJsonFile.file} does not have publishConfig.access: 'public'`, `Package ${packageJsonFile.file} does not have publishConfig.access: 'public'`,

View file

@ -10,7 +10,9 @@ import type {HandlerEvent, HandlerResponse} from '@netlify/functions';
const CookieName = 'DocusaurusPlaygroundName'; const CookieName = 'DocusaurusPlaygroundName';
const PlaygroundConfigs = { const PlaygroundConfigs = {
// codesandbox: 'https://codesandbox.io/s/docusaurus', // Do not use this one, see https://github.com/codesandbox/codesandbox-client/issues/5683#issuecomment-1023252459 // Do not use this one, see
// https://github.com/codesandbox/codesandbox-client/issues/5683#issuecomment-1023252459
// codesandbox: 'https://codesandbox.io/s/docusaurus',
codesandbox: codesandbox:
'https://codesandbox.io/s/github/facebook/docusaurus/tree/main/examples/classic', 'https://codesandbox.io/s/github/facebook/docusaurus/tree/main/examples/classic',
@ -69,14 +71,11 @@ export function readPlaygroundName(
: {}; : {};
const playgroundName: string | undefined = parsedCookie[CookieName]; const playgroundName: string | undefined = parsedCookie[CookieName];
if (playgroundName) { if (!isValidPlaygroundName(playgroundName)) {
if (isValidPlaygroundName(playgroundName)) { console.error(
return playgroundName; `playgroundName found in cookie was invalid: ${playgroundName}`,
} else { );
console.error( return undefined;
`playgroundName found in cookie was invalid: ${playgroundName}`,
);
}
} }
return undefined; return playgroundName;
} }

View file

@ -9,6 +9,6 @@ import type {Handler} from '@netlify/functions';
import {createPlaygroundResponse} from '../functionUtils/playgroundUtils'; import {createPlaygroundResponse} from '../functionUtils/playgroundUtils';
export const handler: Handler = async function (_event, _context) { export const handler: Handler = async function handler(_event, _context) {
return createPlaygroundResponse('codesandbox'); return createPlaygroundResponse('codesandbox');
}; };

View file

@ -9,6 +9,6 @@ import type {Handler} from '@netlify/functions';
import {createPlaygroundResponse} from '../functionUtils/playgroundUtils'; import {createPlaygroundResponse} from '../functionUtils/playgroundUtils';
export const handler: Handler = async function (_event, _context) { export const handler: Handler = async function handler(_event, _context) {
return createPlaygroundResponse('stackblitz'); return createPlaygroundResponse('stackblitz');
}; };

View file

@ -26,16 +26,16 @@ async function generateTemplateExample(template) {
`generating ${template} template for codesandbox in the examples folder...`, `generating ${template} template for codesandbox in the examples folder...`,
); );
// run the docusaurus script to bootstrap the template in the examples folder // run the docusaurus script to create the template in the examples folder
const command = template.endsWith('-typescript') const command = template.endsWith('-typescript')
? template.replace('-typescript', ' -- --typescript') ? template.replace('-typescript', ' -- --typescript')
: template; : template;
shell.exec( shell.exec(
// /!\ we use the published init script on purpose, // /!\ we use the published init script on purpose,
// because using the local init script is too early and could generate upcoming/unavailable config options // because using the local init script is too early and could generate
// remember CodeSandbox templates will use the published version, not the repo version // upcoming/unavailable config options. Remember CodeSandbox templates
// will use the published version, not the repo version
`npm init docusaurus@latest examples/${template} ${command}`, `npm init docusaurus@latest examples/${template} ${command}`,
// `node ./packages/docusaurus-init/bin/index.js init examples/${template} ${template}`,
); );
// read the content of the package.json // read the content of the package.json
@ -49,10 +49,10 @@ async function generateTemplateExample(template) {
// these example projects are not meant to be published to npm // these example projects are not meant to be published to npm
templatePackageJson.private = true; templatePackageJson.private = true;
// make sure package.json name is not "examples-classic" // Make sure package.json name is not "examples-classic". The package.json
// the package.json name appear in CodeSandbox UI so let's display a good name! // name appears in CodeSandbox UI so let's display a good name!
// unfortunately we can't use uppercase or spaces // Unfortunately we can't use uppercase or spaces... See also
// see also https://github.com/codesandbox/codesandbox-client/pull/5136#issuecomment-763521662 // https://github.com/codesandbox/codesandbox-client/pull/5136#issuecomment-763521662
templatePackageJson.name = templatePackageJson.name =
template === 'classic' ? 'docusaurus' : `docusaurus-${template}`; template === 'classic' ? 'docusaurus' : `docusaurus-${template}`;
templatePackageJson.description = templatePackageJson.description =
@ -98,12 +98,13 @@ async function generateTemplateExample(template) {
} }
} }
/* /**
Starters are repositories/branches that only contains a newly initialized Docusaurus site * Starters are repositories/branches that only contains a newly initialized
Those are useful for users to inspect (may be more convenient than "examples/classic) * Docusaurus site. Those are useful for users to inspect (may be more
Also some tools like Netlify deploy button currently require using the main branch of a dedicated repo * convenient than "examples/classic) Also some tools like Netlify deploy button
See https://github.com/jamstack/jamstack.org/pull/609 * currently require using the main branch of a dedicated repo.
Button visible here: https://jamstack.org/generators/ * See https://github.com/jamstack/jamstack.org/pull/609
* Button visible here: https://jamstack.org/generators/
*/ */
function updateStarters() { function updateStarters() {
function forcePushGitSubtree({subfolder, remote, remoteBranch}) { function forcePushGitSubtree({subfolder, remote, remoteBranch}) {

View file

@ -39,7 +39,8 @@ await Promise.all(
}), }),
); );
// You should also run optimizt `find website/src/data/showcase -type f -name '*.png'`. // You should also run
// This is not included here because @funboxteam/optimizt doesn't seem to play well with M1 // optimizt `find website/src/data/showcase -type f -name '*.png'`.
// so I had to run this in a Rosetta terminal. // This is not included here because @funboxteam/optimizt doesn't seem to play
// well with M1 so I had to run this in a Rosetta terminal.
// TODO integrate this as part of the script // TODO integrate this as part of the script

View file

@ -40,7 +40,7 @@ export default {
'@docusaurus/core/lib/client/exports/$1', '@docusaurus/core/lib/client/exports/$1',
// Maybe point to a fixture? // Maybe point to a fixture?
'@generated/.*': '<rootDir>/jest/emptyModule.js', '@generated/.*': '<rootDir>/jest/emptyModule.js',
// TODO maybe use "projects" + multiple configs if we plan to add tests to another theme? // TODO use "projects" + multiple configs if we work on another theme?
'@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1', '@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1',
'@site/(.*)': 'website/$1', '@site/(.*)': 'website/$1',

View file

@ -81,7 +81,8 @@ async function copyTemplate(
) { ) {
await fs.copy(path.resolve(templatesDir, 'shared'), dest); await fs.copy(path.resolve(templatesDir, 'shared'), dest);
// TypeScript variants will copy duplicate resources like CSS & config from base template // TypeScript variants will copy duplicate resources like CSS & config from
// base template
const tsBaseTemplate = getTypeScriptBaseTemplate(template); const tsBaseTemplate = getTypeScriptBaseTemplate(template);
if (tsBaseTemplate) { if (tsBaseTemplate) {
const tsBaseTemplatePath = path.resolve(templatesDir, tsBaseTemplate); const tsBaseTemplatePath = path.resolve(templatesDir, tsBaseTemplate);
@ -94,7 +95,8 @@ async function copyTemplate(
} }
await fs.copy(path.resolve(templatesDir, template), dest, { await fs.copy(path.resolve(templatesDir, template), dest, {
// Symlinks don't exist in published NPM packages anymore, so this is only to prevent errors during local testing // Symlinks don't exist in published NPM packages anymore, so this is only
// to prevent errors during local testing
filter: (filePath) => !fs.lstatSync(filePath).isSymbolicLink(), filter: (filePath) => !fs.lstatSync(filePath).isSymbolicLink(),
}); });
} }
@ -278,7 +280,8 @@ export default async function init(
shell.exec(useYarn ? 'yarn' : 'npm install --color always', { shell.exec(useYarn ? 'yarn' : 'npm install --color always', {
env: { env: {
...process.env, ...process.env,
// Force coloring the output, since the command is invoked by shelljs, which is not the interactive shell // Force coloring the output, since the command is invoked by shelljs,
// which is not the interactive shell
...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}), ...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}),
}, },
}).code !== 0 }).code !== 0

View file

@ -6,12 +6,17 @@
*/ */
/** /**
* This PostCSS plugin will remove duplicate/same custom properties (which are actually overridden ones) **only** from `:root` selector. * This PostCSS plugin will remove duplicate/same custom properties (which are
* actually overridden ones) **only** from `:root` selector.
* *
* Depending on the presence of an `!important` rule in value of custom property, the following actions will happens: * Depending on the presence of an `!important` rule in value of custom
* property, the following actions will happen:
* *
* - If the same custom properties do **not** have an `!important` rule, then all of them will be removed except for the last one (which will actually be applied). * - If the same custom properties do **not** have an `!important` rule, then
* - If the same custom properties have at least one `!important` rule, then only those properties that do not have this rule will be removed. * all of them will be removed except for the last one (which will actually be
* applied).
* - If the same custom properties have at least one `!important` rule, then
* only those properties that do not have this rule will be removed.
* @returns {import('postcss').Plugin} * @returns {import('postcss').Plugin}
*/ */
module.exports = function creator() { module.exports = function creator() {

View file

@ -23,7 +23,7 @@ function interpolate(
values.forEach((value, idx) => { values.forEach((value, idx) => {
const flag = msgs[idx].match(/[a-z]+=$/); const flag = msgs[idx].match(/[a-z]+=$/);
res += msgs[idx].replace(/[a-z]+=$/, ''); res += msgs[idx].replace(/[a-z]+=$/, '');
const format = (function () { const format = (() => {
if (!flag) { if (!flag) {
return (a: string | number) => a; return (a: string | number) => a;
} }

View file

@ -49,9 +49,12 @@ type Options = RemarkAndRehypePluginOptions & {
filepath: string; filepath: string;
}; };
// When this throws, it generally means that there's no metadata file associated with this MDX document /**
// It can happen when using MDX partials (usually starting with _) * When this throws, it generally means that there's no metadata file associated
// That's why it's important to provide the "isMDXPartial" function in config * with this MDX document. It can happen when using MDX partials (usually
* starting with _). That's why it's important to provide the `isMDXPartial`
* function in config
*/
async function readMetadataPath(metadataPath: string) { async function readMetadataPath(metadataPath: string) {
try { try {
return await readFile(metadataPath, 'utf8'); return await readFile(metadataPath, 'utf8');
@ -62,11 +65,14 @@ async function readMetadataPath(metadataPath: string) {
} }
} }
// Converts assets an object with Webpack require calls code /**
// This is useful for mdx files to reference co-located assets using relative paths * Converts assets an object with Webpack require calls code.
// Those assets should enter the Webpack assets pipeline and be hashed * This is useful for mdx files to reference co-located assets using relative
// For now, we only handle that for images and paths starting with ./ * paths. Those assets should enter the Webpack assets pipeline and be hashed.
// {image: "./myImage.png"} => {image: require("./myImage.png")} * For now, we only handle that for images and paths starting with `./`:
*
* `{image: "./myImage.png"}` => `{image: require("./myImage.png")}`
*/
function createAssetsExportCode(assets: Record<string, unknown>) { function createAssetsExportCode(assets: Record<string, unknown>) {
if (Object.keys(assets).length === 0) { if (Object.keys(assets).length === 0) {
return 'undefined'; return 'undefined';
@ -148,7 +154,7 @@ export default async function mdxLoader(
filepath: filePath, filepath: filePath,
}; };
let result; let result: string;
try { try {
result = await mdx(content, options); result = await mdx(content, options);
} catch (err) { } catch (err) {
@ -156,7 +162,7 @@ export default async function mdxLoader(
} }
// MDX partials are MDX files starting with _ or in a folder starting with _ // MDX partials are MDX files starting with _ or in a folder starting with _
// Partial are not expected to have an associated metadata file or front matter // Partial are not expected to have associated metadata files or front matter
const isMDXPartial = options.isMDXPartial && options.isMDXPartial(filePath); const isMDXPartial = options.isMDXPartial && options.isMDXPartial(filePath);
if (isMDXPartial && hasFrontMatter) { if (isMDXPartial && hasFrontMatter) {
const errorMessage = `Docusaurus MDX partial files should not contain FrontMatter. const errorMessage = `Docusaurus MDX partial files should not contain FrontMatter.
@ -168,9 +174,8 @@ ${JSON.stringify(frontMatter, null, 2)}`;
const shouldError = process.env.NODE_ENV === 'test' || process.env.CI; const shouldError = process.env.NODE_ENV === 'test' || process.env.CI;
if (shouldError) { if (shouldError) {
return callback(new Error(errorMessage)); return callback(new Error(errorMessage));
} else {
logger.warn(errorMessage);
} }
logger.warn(errorMessage);
} }
} }

View file

@ -57,7 +57,7 @@ describe('headings plugin', () => {
test('should not overwrite `data` on headings', () => { test('should not overwrite `data` on headings', () => {
const result = process('# Normal\n', [ const result = process('# Normal\n', [
function () { () => {
function transform(tree) { function transform(tree) {
tree.children[0].data = {foo: 'bar'}; tree.children[0].data = {foo: 'bar'};
} }
@ -80,7 +80,7 @@ describe('headings plugin', () => {
test('should not overwrite `data.hProperties` on headings', () => { test('should not overwrite `data.hProperties` on headings', () => {
const result = process('# Normal\n', [ const result = process('# Normal\n', [
function () { () => {
function transform(tree) { function transform(tree) {
tree.children[0].data = {hProperties: {className: ['foo']}}; tree.children[0].data = {hProperties: {className: ['foo']}};
} }
@ -110,7 +110,7 @@ describe('headings plugin', () => {
'## Something also', '## Something also',
].join('\n\n'), ].join('\n\n'),
[ [
function () { () => {
function transform(tree) { function transform(tree) {
tree.children[1].data = {hProperties: {id: 'here'}}; tree.children[1].data = {hProperties: {id: 'here'}};
tree.children[3].data = {hProperties: {id: 'something'}}; tree.children[3].data = {hProperties: {id: 'something'}};

View file

@ -44,7 +44,8 @@ function headings(): Transformer {
if (parsedHeading.id) { if (parsedHeading.id) {
// When there's an id, it is always in the last child node // When there's an id, it is always in the last child node
// Sometimes heading is in multiple "parts" (** syntax creates a child node): // Sometimes heading is in multiple "parts" (** syntax creates a child
// node):
// ## part1 *part2* part3 {#id} // ## part1 *part2* part3 {#id}
const lastNode = headingNode.children[ const lastNode = headingNode.children[
headingNode.children.length - 1 headingNode.children.length - 1

View file

@ -46,9 +46,9 @@ export default function search(node: Node): TOCItem[] {
}); });
}); });
// Keep track of which previous index would be the current heading's direct parent. // Keep track of which previous index would be the current heading's direct
// Each entry <i> is the last index of the `headings` array at heading level <i>. // parent. Each entry <i> is the last index of the `headings` array at heading
// We will modify these indices as we iterate through all headings. // level <i>. We will modify these indices as we iterate through all headings.
// e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2 // e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2
// indices 0 and 1 will remain unused. // indices 0 and 1 will remain unused.
const prevIndexForLevel = Array(7).fill(-1); const prevIndexForLevel = Array(7).fill(-1);

View file

@ -116,14 +116,10 @@ async function getImageAbsolutePath(
} }
return imageFilePath; return imageFilePath;
} }
// We try to convert image urls without protocol to images with require calls // relative paths are resolved against the source file's folder
// going through webpack ensures that image assets exist at build time const imageFilePath = path.join(path.dirname(filePath), imagePath);
else { await ensureImageFileExist(imageFilePath, filePath);
// relative paths are resolved against the source file's folder return imageFilePath;
const imageFilePath = path.join(path.dirname(filePath), imagePath);
await ensureImageFileExist(imageFilePath, filePath);
return imageFilePath;
}
} }
async function processImageNode(node: Image, context: Context) { async function processImageNode(node: Image, context: Context) {
@ -137,16 +133,16 @@ async function processImageNode(node: Image, context: Context) {
const parsedUrl = url.parse(node.url); const parsedUrl = url.parse(node.url);
if (parsedUrl.protocol || !parsedUrl.pathname) { if (parsedUrl.protocol || !parsedUrl.pathname) {
// pathname:// is an escape hatch, // pathname:// is an escape hatch, in case user does not want her images to
// in case user does not want his images to be converted to require calls going through webpack loader // be converted to require calls going through webpack loader
// we don't have to document this for now,
// it's mostly to make next release less risky (2.0.0-alpha.59)
if (parsedUrl.protocol === 'pathname:') { if (parsedUrl.protocol === 'pathname:') {
node.url = node.url.replace('pathname://', ''); node.url = node.url.replace('pathname://', '');
} }
return; return;
} }
// We try to convert image urls without protocol to images with require calls
// going through webpack ensures that image assets exist at build time
const imagePath = await getImageAbsolutePath(parsedUrl.pathname, context); const imagePath = await getImageAbsolutePath(parsedUrl.pathname, context);
await toImageRequireNode(node, imagePath, context.filePath); await toImageRequireNode(node, imagePath, context.filePath);
} }

View file

@ -10,9 +10,10 @@ import type {Transformer, Processor} from 'unified';
import type {Code, Parent} from 'mdast'; import type {Code, Parent} from 'mdast';
// This plugin is mostly to help integrating Docusaurus with translation systems // This plugin is mostly to help integrating Docusaurus with translation systems
// that do not support well MDX embedded JSX syntax (like Crowdin) // that do not support well MDX embedded JSX syntax (like Crowdin).
// We wrap the JSX syntax in code blocks so that translation tools don't mess-up with the markup // We wrap the JSX syntax in code blocks so that translation tools don't mess up
// But the JSX inside such code blocks should still be evaluated as JSX // with the markup, but the JSX inside such code blocks should still be
// evaluated as JSX
// See https://github.com/facebook/docusaurus/pull/4278 // See https://github.com/facebook/docusaurus/pull/4278
function plugin(this: Processor): Transformer { function plugin(this: Processor): Transformer {
const transformer: Transformer = (root) => { const transformer: Transformer = (root) => {

View file

@ -61,7 +61,8 @@ function sanitizedFileContent(
return sanitizedData; return sanitizedData;
} }
// TODO refactor this new type should be used everywhere instead of passing many params to each method // TODO refactor this new type should be used everywhere instead of passing many
// params to each method
type MigrationContext = { type MigrationContext = {
siteDir: string; siteDir: string;
newDir: string; newDir: string;

View file

@ -179,13 +179,13 @@ declare module '@docusaurus/Interpolate' {
Value extends ReactNode, Value extends ReactNode,
> = Record<ExtractInterpolatePlaceholders<Str>, Value>; > = Record<ExtractInterpolatePlaceholders<Str>, Value>;
// TS function overload: if all the values are plain strings, then interpolate returns a simple string // If all the values are plain strings, interpolate returns a simple string
export function interpolate<Str extends string>( export function interpolate<Str extends string>(
text: Str, text: Str,
values?: InterpolateValues<Str, string | number>, values?: InterpolateValues<Str, string | number>,
): string; ): string;
// If values contain any ReactNode, then the return is a ReactNode // If values contain any ReactNode, the return is a ReactNode
export function interpolate<Str extends string, Value extends ReactNode>( export function interpolate<Str extends string, Value extends ReactNode>(
text: Str, text: Str,
values?: InterpolateValues<Str, Value>, values?: InterpolateValues<Str, Value>,

View file

@ -114,7 +114,9 @@ describe('toRedirectFilesMetadata', () => {
); );
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([ expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
// path.join(pluginContext.outDir, '/abc.html/index.html'), // Can't be used because /abc.html already exists, and file/folder can't share same name on Unix! // Can't be used because /abc.html already exists, and file/folder can't
// share same name on Unix!
// path.join(pluginContext.outDir, '/abc.html/index.html'),
path.join(pluginContext.outDir, '/abc.html.html'), // Weird but on purpose! path.join(pluginContext.outDir, '/abc.html.html'), // Weird but on purpose!
path.join(pluginContext.outDir, '/def/index.html'), path.join(pluginContext.outDir, '/def/index.html'),
path.join(pluginContext.outDir, '/xyz/index.html'), path.join(pluginContext.outDir, '/xyz/index.html'),

View file

@ -39,9 +39,12 @@ export default function collectRedirects(
} }
// If users wants to redirect to=/abc and they enable trailingSlash=true then // If users wants to redirect to=/abc and they enable trailingSlash=true then
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid path now) // => we don't want to reject the to=/abc (as only /abc/ is an existing/valid
// => we want to redirect to=/abc/ without the user having to change all its redirect plugin options // path now)
// It should be easy to toggle siteConfig.trailingSlash option without having to change other configs // => we want to redirect to=/abc/ without the user having to change all its
// redirect plugin options
// It should be easy to toggle siteConfig.trailingSlash option without having to
// change other configs
function applyRedirectsTrailingSlash( function applyRedirectsTrailingSlash(
redirects: RedirectMetadata[], redirects: RedirectMetadata[],
params: ApplyTrailingSlashParams, params: ApplyTrailingSlashParams,

View file

@ -81,19 +81,12 @@ export function createFromExtensionsRedirects(
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) { if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
return []; return [];
} }
// /path => /path.html
// /path/ => /path.html/
function getFrom(ext: string) {
if (path.endsWith('/')) {
return addTrailingSlash(`${removeTrailingSlash(path)}.${ext}`);
} else {
return `${path}.${ext}`;
}
}
return extensions.map((ext) => ({ return extensions.map((ext) => ({
from: getFrom(ext), // /path => /path.html
// /path/ => /path.html/
from: path.endsWith('/')
? addTrailingSlash(`${removeTrailingSlash(path)}.${ext}`)
: `${path}.${ext}`,
to: path, to: path,
})); }));
}; };

View file

@ -25,7 +25,8 @@ export function createToUrl(baseUrl: string, to: string): string {
} }
// Create redirect file path // Create redirect file path
// Make sure this path has lower precedence over the original file path when served by host providers! // Make sure this path has lower precedence over the original file path when
// served by host providers!
// Otherwise it can produce infinite redirect loops! // Otherwise it can produce infinite redirect loops!
// //
// See https://github.com/facebook/docusaurus/issues/5055 // See https://github.com/facebook/docusaurus/issues/5055
@ -39,17 +40,19 @@ function getRedirectFilePath(
const filePath = path.dirname(fromPath); const filePath = path.dirname(fromPath);
// Edge case for https://github.com/facebook/docusaurus/pull/5102 // Edge case for https://github.com/facebook/docusaurus/pull/5102
// If the redirect source path is /xyz, with file /xyz.html // If the redirect source path is /xyz, with file /xyz.html
// We can't write the redirect file at /xyz.html/index.html because for Unix FS, a file/folder can't have the same name "xyz.html" // We can't write the redirect file at /xyz.html/index.html because for Unix
// The only possible solution for a redirect file is thus /xyz.html.html (I know, looks suspicious) // FS, a file/folder can't have the same name "xyz.html"
// The only possible solution for a redirect file is thus /xyz.html.html (I
// know, looks suspicious)
if (trailingSlash === false && fileName.endsWith('.html')) { if (trailingSlash === false && fileName.endsWith('.html')) {
return path.join(filePath, `${fileName}.html`); return path.join(filePath, `${fileName}.html`);
} }
// If the target path is /xyz, with file /xyz/index.html, we don't want the redirect file to be /xyz.html // If the target path is /xyz, with file /xyz/index.html, we don't want the
// otherwise it would be picked in priority and the redirect file would redirect to itself // redirect file to be /xyz.html, otherwise it would be picked in priority and
// We prefer the redirect file to be /xyz.html/index.html, served with lower priority for most static hosting tools // the redirect file would redirect to itself. We prefer the redirect file to
else { // be /xyz.html/index.html, served with lower priority for most static hosting
return path.join(filePath, `${fileName}/index.html`); // tools
} return path.join(filePath, `${fileName}/index.html`);
} }
export function toRedirectFilesMetadata( export function toRedirectFilesMetadata(

View file

@ -83,9 +83,9 @@ function normalizeFrontMatterAuthors(
authorInput: string | BlogPostFrontMatterAuthor, authorInput: string | BlogPostFrontMatterAuthor,
): BlogPostFrontMatterAuthor { ): BlogPostFrontMatterAuthor {
if (typeof authorInput === 'string') { if (typeof authorInput === 'string') {
// Technically, we could allow users to provide an author's name here // Technically, we could allow users to provide an author's name here, but
// IMHO it's better to only support keys here // we only support keys, otherwise, a typo in a key would fallback to
// Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed // becoming a name and may end up unnoticed
return {key: authorInput}; return {key: authorInput};
} }
return authorInput; return authorInput;
@ -137,7 +137,8 @@ export function getBlogPostAuthors(params: AuthorsParam): Author[] {
const authors = getFrontMatterAuthors(params); const authors = getFrontMatterAuthors(params);
if (authorLegacy) { if (authorLegacy) {
// Technically, we could allow mixing legacy/authors front matter, but do we really want to? // Technically, we could allow mixing legacy/authors front matter, but do we
// really want to?
if (authors.length > 0) { if (authors.length > 0) {
throw new Error( throw new Error(
`To declare blog post authors, use the 'authors' front matter in priority. `To declare blog post authors, use the 'authors' front matter in priority.

View file

@ -82,11 +82,10 @@ export function parseBlogFileName(
const slugDate = dateString.replace(/-/g, '/'); const slugDate = dateString.replace(/-/g, '/');
const slug = `/${slugDate}/${folder}${text}`; const slug = `/${slugDate}/${folder}${text}`;
return {date, text, slug}; return {date, text, slug};
} else {
const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
const slug = `/${text}`;
return {date: undefined, text, slug};
} }
const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
const slug = `/${text}`;
return {date: undefined, text, slug};
} }
function formatBlogPostDate(locale: string, date: Date): string { function formatBlogPostDate(locale: string, date: Date): string {

View file

@ -322,7 +322,8 @@ export default async function pluginContentBlog(
modules: { modules: {
sidebar: aliasedSource(sidebarProp), sidebar: aliasedSource(sidebarProp),
items: items.map((postID) => items: items.map((postID) =>
// To tell routes.js this is an import and not a nested object to recurse. // To tell routes.js this is an import and not a nested object
// to recurse.
({ ({
content: { content: {
__import: true, __import: true,
@ -485,7 +486,8 @@ export default async function pluginContentBlog(
// Blog posts title are rendered separately // Blog posts title are rendered separately
removeContentTitle: true, removeContentTitle: true,
// Assets allow to convert some relative images paths to require() calls // Assets allow to convert some relative images paths to
// require() calls
createAssets: ({ createAssets: ({
frontMatter, frontMatter,
metadata, metadata,

View file

@ -95,7 +95,8 @@ Entries created:
}, },
expectSnapshot: () => { expectSnapshot: () => {
// Sort the route config like in src/server/plugins/index.ts for consistent snapshot ordering // Sort the route config like in src/server/plugins/index.ts for
// consistent snapshot ordering
sortConfig(routeConfigs); sortConfig(routeConfigs);
expect(routeConfigs).not.toEqual([]); expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot('route config'); expect(routeConfigs).toMatchSnapshot('route config');
@ -249,7 +250,8 @@ describe('simple website', () => {
.spyOn(cliDocs, 'cliDocsVersionCommand') .spyOn(cliDocs, 'cliDocsVersionCommand')
.mockImplementation(); .mockImplementation();
const cli = new commander.Command(); const cli = new commander.Command();
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command // @ts-expect-error: in actual usage, we pass the static commander instead
// of the new command
plugin.extendCli!(cli); plugin.extendCli!(cli);
cli.parse(['node', 'test', 'docs:version', '1.0.0']); cli.parse(['node', 'test', 'docs:version', '1.0.0']);
expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledTimes(1);
@ -373,7 +375,8 @@ describe('versioned website', () => {
.spyOn(cliDocs, 'cliDocsVersionCommand') .spyOn(cliDocs, 'cliDocsVersionCommand')
.mockImplementation(); .mockImplementation();
const cli = new commander.Command(); const cli = new commander.Command();
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command // @ts-expect-error: in actual usage, we pass the static commander instead
// of the new command
plugin.extendCli!(cli); plugin.extendCli!(cli);
cli.parse(['node', 'test', 'docs:version', '2.0.0']); cli.parse(['node', 'test', 'docs:version', '2.0.0']);
expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledTimes(1);
@ -522,7 +525,8 @@ describe('versioned website (community)', () => {
.spyOn(cliDocs, 'cliDocsVersionCommand') .spyOn(cliDocs, 'cliDocsVersionCommand')
.mockImplementation(); .mockImplementation();
const cli = new commander.Command(); const cli = new commander.Command();
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command // @ts-expect-error: in actual usage, we pass the static commander instead
// of the new command
plugin.extendCli!(cli); plugin.extendCli!(cli);
cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']); cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledTimes(1);
@ -726,7 +730,8 @@ describe('site with partial autogenerated sidebars', () => {
const {content} = await loadSite(); const {content} = await loadSite();
const version = content.loadedVersions[0]; const version = content.loadedVersions[0];
// Only looking at the docs of the autogen sidebar, others metadata should not be affected // Only looking at the docs of the autogen sidebar, others metadata should
// not be affected
expect(getDocById(version, 'API/api-end')).toMatchSnapshot(); expect(getDocById(version, 'API/api-end')).toMatchSnapshot();
expect(getDocById(version, 'API/api-overview')).toMatchSnapshot(); expect(getDocById(version, 'API/api-overview')).toMatchSnapshot();
@ -803,7 +808,8 @@ describe('site with custom sidebar items generator', () => {
const generatorArg: SidebarItemsGeneratorOptionArgs = const generatorArg: SidebarItemsGeneratorOptionArgs =
customSidebarItemsGeneratorMock.mock.calls[0][0]; customSidebarItemsGeneratorMock.mock.calls[0][0];
// Make test pass even if docs are in different order and paths are absolutes // Make test pass even if docs are in different order and paths are
// absolutes
function makeDeterministic( function makeDeterministic(
arg: SidebarItemsGeneratorOptionArgs, arg: SidebarItemsGeneratorOptionArgs,
): SidebarItemsGeneratorOptionArgs { ): SidebarItemsGeneratorOptionArgs {

View file

@ -32,10 +32,12 @@ function createVersionedSidebarFile({
version: string; version: string;
}) { }) {
// Load current sidebar and create a new versioned sidebars file (if needed). // Load current sidebar and create a new versioned sidebars file (if needed).
// Note: we don't need the sidebars file to be normalized: it's ok to let plugin option changes to impact older, versioned sidebars // Note: we don't need the sidebars file to be normalized: it's ok to let
// plugin option changes to impact older, versioned sidebars
const sidebars = loadSidebarsFile(sidebarPath); const sidebars = loadSidebarsFile(sidebarPath);
// Do not create a useless versioned sidebars file if sidebars file is empty or sidebars are disabled/false) // Do not create a useless versioned sidebars file if sidebars file is empty
// or sidebars are disabled/false)
const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0; const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0;
if (shouldCreateVersionedSidebarFile) { if (shouldCreateVersionedSidebarFile) {

View file

@ -28,7 +28,7 @@ export function getActivePlugin(
options: GetActivePluginOptions = {}, options: GetActivePluginOptions = {},
): ActivePlugin | undefined { ): ActivePlugin | undefined {
const activeEntry = Object.entries(allPluginDatas) const activeEntry = Object.entries(allPluginDatas)
// A quick route sorting: '/android/foo' should match '/android' instead of '/' // Route sorting: '/android/foo' should match '/android' instead of '/'
.sort((a, b) => b[1].path.localeCompare(a[1].path)) .sort((a, b) => b[1].path.localeCompare(a[1].path))
.find( .find(
([, pluginData]) => ([, pluginData]) =>
@ -67,7 +67,7 @@ export const getActiveVersion = (
): GlobalVersion | undefined => { ): GlobalVersion | undefined => {
const lastVersion = getLatestVersion(data); const lastVersion = getLatestVersion(data);
// Last version is a route like /docs/*, // Last version is a route like /docs/*,
// we need to try to match it last or it would match /docs/version-1.0/* as well // we need to match it last or it would match /docs/version-1.0/* as well
const orderedVersionsMetadata = [ const orderedVersionsMetadata = [
...data.versions.filter((version) => version !== lastVersion), ...data.versions.filter((version) => version !== lastVersion),
lastVersion, lastVersion,

View file

@ -27,12 +27,13 @@ import type {
GetActivePluginOptions, GetActivePluginOptions,
} from '@docusaurus/plugin-content-docs/client'; } from '@docusaurus/plugin-content-docs/client';
// Important to use a constant object to avoid React useEffect executions etc..., // Important to use a constant object to avoid React useEffect executions etc.
// see https://github.com/facebook/docusaurus/issues/5089 // see https://github.com/facebook/docusaurus/issues/5089
const StableEmptyObject = {}; const StableEmptyObject = {};
// Not using useAllPluginInstancesData() because in blog-only mode, docs hooks are still used by the theme // Not using useAllPluginInstancesData() because in blog-only mode, docs hooks
// We need a fail-safe fallback when the docs plugin is not in use // are still used by the theme. We need a fail-safe fallback when the docs
// plugin is not in use
export const useAllDocsData = (): Record<string, GlobalPluginData> => export const useAllDocsData = (): Record<string, GlobalPluginData> =>
// useAllPluginInstancesData('docusaurus-plugin-content-docs'); // useAllPluginInstancesData('docusaurus-plugin-content-docs');
useGlobalData()['docusaurus-plugin-content-docs'] ?? StableEmptyObject; useGlobalData()['docusaurus-plugin-content-docs'] ?? StableEmptyObject;

View file

@ -139,7 +139,8 @@ function doProcessDocMetadata({
const { const {
custom_edit_url: customEditURL, custom_edit_url: customEditURL,
// Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default, // Strip number prefixes by default
// (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc)
// but allow to disable this behavior with front matter // but allow to disable this behavior with front matter
parse_number_prefixes: parseNumberPrefixes = true, parse_number_prefixes: parseNumberPrefixes = true,
} = frontMatter; } = frontMatter;
@ -164,7 +165,8 @@ function doProcessDocMetadata({
throw new Error(`Document id "${baseID}" cannot include slash.`); throw new Error(`Document id "${baseID}" cannot include slash.`);
} }
// For autogenerated sidebars, sidebar position can come from filename number prefix or front matter // For autogenerated sidebars, sidebar position can come from filename number
// prefix or front matter
const sidebarPosition: number | undefined = const sidebarPosition: number | undefined =
frontMatter.sidebar_position ?? numberPrefix; frontMatter.sidebar_position ?? numberPrefix;
@ -205,8 +207,9 @@ function doProcessDocMetadata({
numberPrefixParser: options.numberPrefixParser, numberPrefixParser: options.numberPrefixParser,
}); });
// Note: the title is used by default for page title, sidebar label, pagination buttons... // Note: the title is used by default for page title, sidebar label,
// frontMatter.title should be used in priority over contentTitle (because it can contain markdown/JSX syntax) // pagination buttons... frontMatter.title should be used in priority over
// contentTitle (because it can contain markdown/JSX syntax)
const title: string = frontMatter.title ?? contentTitle ?? baseID; const title: string = frontMatter.title ?? contentTitle ?? baseID;
const description: string = frontMatter.description ?? excerpt ?? ''; const description: string = frontMatter.description ?? excerpt ?? '';
@ -233,9 +236,8 @@ function doProcessDocMetadata({
? versionMetadata.versionEditUrlLocalized ? versionMetadata.versionEditUrlLocalized
: versionMetadata.versionEditUrl; : versionMetadata.versionEditUrl;
return getEditUrl(relativeFilePath, baseVersionEditUrl); return getEditUrl(relativeFilePath, baseVersionEditUrl);
} else {
return undefined;
} }
return undefined;
} }
// Assign all of object properties during instantiation (if possible) for // Assign all of object properties during instantiation (if possible) for
@ -361,9 +363,8 @@ export function getMainDocId({
doc.id === firstDocIdOfFirstSidebar || doc.id === firstDocIdOfFirstSidebar ||
doc.unversionedId === firstDocIdOfFirstSidebar, doc.unversionedId === firstDocIdOfFirstSidebar,
)!; )!;
} else {
return docs[0];
} }
return docs[0];
} }
return getMainDoc().unversionedId; return getMainDoc().unversionedId;
@ -407,7 +408,8 @@ export function toCategoryIndexMatcherParam({
} }
/** /**
* guides/sidebar/autogenerated.md -> 'autogenerated', '.md', ['sidebar', 'guides'] * `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/ */
export function splitPath(str: string): { export function splitPath(str: string): {
/** /**
@ -428,15 +430,17 @@ export function splitPath(str: string): {
} }
// Return both doc ids // Return both doc ids
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids // TODO legacy retro-compatibility due to old versioned sidebars using
// ("id" should be removed & "versionedId" should be renamed to "id") // versioned doc ids ("id" should be removed & "versionedId" should be renamed
// to "id")
export function getDocIds(doc: DocMetadataBase): [string, string] { export function getDocIds(doc: DocMetadataBase): [string, string] {
return [doc.unversionedId, doc.id]; return [doc.unversionedId, doc.id];
} }
// docs are indexed by both versioned and unversioned ids at the same time // docs are indexed by both versioned and unversioned ids at the same time
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids // TODO legacy retro-compatibility due to old versioned sidebars using
// ("id" should be removed & "versionedId" should be renamed to "id") // versioned doc ids ("id" should be removed & "versionedId" should be renamed
// to "id")
export function createDocsByIdIndex< export function createDocsByIdIndex<
Doc extends {id: string; unversionedId: string}, Doc extends {id: string; unversionedId: string},
>(docs: Doc[]): Record<string, Doc> { >(docs: Doc[]): Record<string, Doc> {

View file

@ -8,15 +8,16 @@
import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs'; import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs';
// Best-effort to avoid parsing some patterns as number prefix // Best-effort to avoid parsing some patterns as number prefix
const IgnoredPrefixPatterns = (function () { const IgnoredPrefixPatterns = (() => {
// ignore common date-like patterns: https://github.com/facebook/docusaurus/issues/4640 // ignore common date-like patterns: https://github.com/facebook/docusaurus/issues/4640
const DateLikePrefixRegex = const DateLikePrefixRegex =
/^((\d{2}|\d{4})[-_.]\d{2}([-_.](\d{2}|\d{4}))?)(.*)$/; /^((\d{2}|\d{4})[-_.]\d{2}([-_.](\d{2}|\d{4}))?)(.*)$/;
// ignore common versioning patterns: https://github.com/facebook/docusaurus/issues/4653 // ignore common versioning patterns: https://github.com/facebook/docusaurus/issues/4653
// note: we could try to parse float numbers in filenames but that is probably not worth it // note: we could try to parse float numbers in filenames but that is
// as a version such as "8.0" can be interpreted as both a version and a float // probably not worth it as a version such as "8.0" can be interpreted as both
// User can configure his own NumberPrefixParser if he wants 8.0 to be interpreted as a float // a version and a float. User can configure her own NumberPrefixParser if
// she wants 8.0 to be interpreted as a float
const VersionLikePrefixRegex = /^(\d+[-_.]\d+)(.*)$/; const VersionLikePrefixRegex = /^(\d+[-_.]\d+)(.*)$/;
return new RegExp( return new RegExp(

View file

@ -148,8 +148,9 @@ export function validateOptions({
let options = userOptions; let options = userOptions;
if (options.sidebarCollapsible === false) { if (options.sidebarCollapsible === false) {
// When sidebarCollapsible=false and sidebarCollapsed=undefined, we don't want to have the inconsistency warning // When sidebarCollapsible=false and sidebarCollapsed=undefined, we don't
// We let options.sidebarCollapsible become the default value for options.sidebarCollapsed // want to have the inconsistency warning. We let options.sidebarCollapsible
// become the default value for options.sidebarCollapsed
if (typeof options.sidebarCollapsed === 'undefined') { if (typeof options.sidebarCollapsed === 'undefined') {
options = { options = {
...options, ...options,

View file

@ -45,7 +45,8 @@ declare module '@docusaurus/plugin-content-docs' {
sidebarPath?: string | false | undefined; sidebarPath?: string | false | undefined;
}; };
// TODO support custom version banner? {type: "error", content: "html content"} // TODO support custom version banner?
// {type: "error", content: "html content"}
export type VersionBanner = 'unreleased' | 'unmaintained'; export type VersionBanner = 'unreleased' | 'unmaintained';
export type VersionOptions = { export type VersionOptions = {
path?: string; path?: string;

View file

@ -73,7 +73,8 @@ export async function createCategoryGeneratedIndexRoutes({
modules: { modules: {
categoryGeneratedIndex: aliasedSource(propData), categoryGeneratedIndex: aliasedSource(propData),
}, },
// Same as doc, this sidebar route attribute permits to associate this subpage to the given sidebar // Same as doc, this sidebar route attribute permits to associate this
// subpage to the given sidebar
...(sidebar && {sidebar}), ...(sidebar && {sidebar}),
}; };
} }
@ -109,7 +110,8 @@ export async function createDocRoutes({
content: metadataItem.source, content: metadataItem.source,
}, },
// Because the parent (DocPage) comp need to access it easily // Because the parent (DocPage) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state) // This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state)
...(metadataItem.sidebar && { ...(metadataItem.sidebar && {
sidebar: metadataItem.sidebar, sidebar: metadataItem.sidebar,
}), }),

View file

@ -205,7 +205,8 @@ describe('processSidebars', () => {
link: { link: {
type: 'generated-index', type: 'generated-index',
slug: 'generated-cat-index-slug', slug: 'generated-cat-index-slug',
// @ts-expect-error: TODO undefined should be allowed here, typing error needing refactor // @ts-expect-error: TODO undefined should be allowed here,
// typing error needing refactor
permalink: undefined, permalink: undefined,
}, },
}, },

View file

@ -47,7 +47,8 @@ export type CategoryMetadataFile = {
className?: string; className?: string;
link?: SidebarItemCategoryLinkConfig | null; link?: SidebarItemCategoryLinkConfig | null;
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed? // TODO should we allow "items" here? how would this work? would an
// "autogenerated" type be allowed?
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/ // This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
// cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199 // cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199
}; };
@ -56,16 +57,20 @@ type WithPosition<T> = T & {position?: number};
/** /**
* A representation of the fs structure. For each object entry: * A representation of the fs structure. For each object entry:
* If it's a folder, the key is the directory name, and value is the directory content; * If it's a folder, the key is the directory name, and value is the directory
* If it's a doc file, the key is the doc id prefixed with '$doc$/', and value is null * content; If it's a doc file, the key is the doc id prefixed with '$doc$/',
* and value is null
*/ */
type Dir = { type Dir = {
[item: string]: Dir | null; [item: string]: Dir | null;
}; };
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata // TODO I now believe we should read all the category metadata files ahead of
// Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory... // time: we may need this metadata to customize docs metadata
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it // Example use-case being able to disable number prefix parsing at the folder
// level, or customize the default base slug for an intermediate directory
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may
// want to read the metadata as yaml on it
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449 // see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
async function readCategoryMetadataFile( async function readCategoryMetadataFile(
categoryDirPath: string, categoryDirPath: string,
@ -142,7 +147,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
* Step 2. Turn the linear file list into a tree structure. * Step 2. Turn the linear file list into a tree structure.
*/ */
function treeify(docs: SidebarItemsGeneratorDoc[]): Dir { function treeify(docs: SidebarItemsGeneratorDoc[]): Dir {
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item) // Get the category breadcrumb of a doc (relative to the dir of the
// autogenerated sidebar item)
// autogenDir=a/b and docDir=a/b/c/d => returns [c, d] // autogenDir=a/b and docDir=a/b/c/d => returns [c, d]
// autogenDir=a/b and docDir=a/b => returns [] // autogenDir=a/b and docDir=a/b => returns []
// TODO: try to use path.relative() // TODO: try to use path.relative()
@ -169,7 +175,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
} }
/** /**
* Step 3. Recursively transform the tree-like file structure to sidebar items. * Step 3. Recursively transform the tree-like structure to sidebar items.
* (From a record to an array of items, akin to normalizing shorthand) * (From a record to an array of items, akin to normalizing shorthand)
*/ */
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> { function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
@ -182,7 +188,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
type: 'doc', type: 'doc',
id, id,
position, position,
// We don't want these fields to magically appear in the generated sidebar // We don't want these fields to magically appear in the generated
// sidebar
...(label !== undefined && {label}), ...(label !== undefined && {label}),
...(className !== undefined && {className}), ...(className !== undefined && {className}),
}; };
@ -225,13 +232,12 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
if (link !== undefined) { if (link !== undefined) {
if (link && link.type === 'doc') { if (link && link.type === 'doc') {
return findDocByLocalId(link.id)?.id || getDoc(link.id).id; return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
} else {
// We don't continue for other link types on purpose!
// IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
return undefined;
} }
// If a link is explicitly specified, we won't apply conventions
return undefined;
} }
// Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc // Apply default convention to pick index.md, README.md or
// <categoryName>.md as the category doc
return findConventionalCategoryDocLink()?.id; return findConventionalCategoryDocLink()?.id;
} }
@ -279,10 +285,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
} }
/** /**
* Step 4. Recursively sort the categories/docs + remove the "position" attribute from final output. * Step 4. Recursively sort the categories/docs + remove the "position"
* Note: the "position" is only used to sort "inside" a sidebar slice. It is not * attribute from final output. Note: the "position" is only used to sort
* used to sort across multiple consecutive sidebar slices (ie a whole Category * "inside" a sidebar slice. It is not used to sort across multiple
* composed of multiple autogenerated items) * consecutive sidebar slices (i.e. a whole category composed of multiple
* autogenerated items)
*/ */
function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] { function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] {
const processedSidebarItems = sidebarItems.map((item) => { const processedSidebarItems = sidebarItems.map((item) => {
@ -298,7 +305,6 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
return sortedSidebarItems.map(({position, ...item}) => item); return sortedSidebarItems.map(({position, ...item}) => item);
} }
// TODO: the whole code is designed for pipeline operator // TODO: the whole code is designed for pipeline operator
// return getAutogenDocs() |> treeify |> await generateSidebar(^) |> sortItems;
const docs = getAutogenDocs(); const docs = getAutogenDocs();
const fsModel = treeify(docs); const fsModel = treeify(docs);
const sidebarWithPosition = await generateSidebar(fsModel); const sidebarWithPosition = await generateSidebar(fsModel);

View file

@ -60,7 +60,8 @@ function toSidebarItemsGeneratorVersion(
return pick(version, ['versionName', 'contentPath']); return pick(version, ['versionName', 'contentPath']);
} }
// Handle the generation of autogenerated sidebar items and other post-processing checks // Handle the generation of autogenerated sidebar items and other
// post-processing checks
async function processSidebar( async function processSidebar(
unprocessedSidebar: NormalizedSidebar, unprocessedSidebar: NormalizedSidebar,
params: SidebarProcessorParams, params: SidebarProcessorParams,
@ -91,7 +92,8 @@ async function processSidebar(
async function processAutoGeneratedItem( async function processAutoGeneratedItem(
item: SidebarItemAutogenerated, item: SidebarItemAutogenerated,
): Promise<SidebarItem[]> { ): Promise<SidebarItem[]> {
// TODO the returned type can't be trusted in practice (generator can be user-provided) // TODO the returned type can't be trusted in practice (generator can be
// user-provided)
const generatedItems = await sidebarItemsGenerator({ const generatedItems = await sidebarItemsGenerator({
item, item,
numberPrefixParser, numberPrefixParser,
@ -106,7 +108,8 @@ async function processSidebar(
normalizeItem(generatedItem, {...params, ...sidebarOptions}), normalizeItem(generatedItem, {...params, ...sidebarOptions}),
); );
// Process again... weird but sidebar item generated might generate some auto-generated items? // Process again... weird but sidebar item generated might generate some
// auto-generated items?
return processItems(generatedItemsNormalized); return processItems(generatedItemsNormalized);
} }

View file

@ -205,7 +205,8 @@ export type SidebarItemsGenerator = (
Promise<SidebarItem[]>; Promise<SidebarItem[]>;
// Promise<SidebarItemConfig[]>; // Promise<SidebarItemConfig[]>;
// Also inject the default generator to conveniently wrap/enhance/sort the default sidebar gen logic // Also inject the default generator to conveniently wrap/enhance/sort the
// default sidebar gen logic
// see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320 // see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
export type SidebarItemsGeneratorOptionArgs = { export type SidebarItemsGeneratorOptionArgs = {
defaultSidebarItemsGenerator: SidebarItemsGenerator; defaultSidebarItemsGenerator: SidebarItemsGenerator;

View file

@ -16,7 +16,6 @@ import type {
SidebarCategoriesShorthand, SidebarCategoriesShorthand,
SidebarItemConfig, SidebarItemConfig,
SidebarItemCategoryWithGeneratedIndex, SidebarItemCategoryWithGeneratedIndex,
SidebarItemCategoryWithLink,
SidebarNavigationItem, SidebarNavigationItem,
} from './types'; } from './types';
@ -46,8 +45,11 @@ export function transformSidebarItems(
return sidebar.map(transformRecursive); return sidebar.map(transformRecursive);
} }
// Flatten sidebar items into a single flat array (containing categories/docs on the same level) /**
// /!\ order matters (useful for next/prev nav), top categories appear before their child elements * Flatten sidebar items into a single flat array (containing categories/docs on
* the same level). Order matters (useful for next/prev nav), top categories
* appear before their child elements
*/
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] { function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
function flattenRecursive(item: SidebarItem): SidebarItem[] { function flattenRecursive(item: SidebarItem): SidebarItem[] {
return item.type === 'category' return item.type === 'category'
@ -196,34 +198,33 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
sidebarName = getSidebarNameByDocId(docId); sidebarName = getSidebarNameByDocId(docId);
} }
if (sidebarName) { if (!sidebarName) {
if (!sidebarNameToNavigationItems[sidebarName]) {
throw new Error(
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
);
}
const navigationItems = sidebarNameToNavigationItems[sidebarName];
const currentItemIndex = navigationItems.findIndex((item) => {
if (item.type === 'doc') {
return item.id === docId;
}
if (item.type === 'category' && item.link.type === 'doc') {
return item.link.id === docId;
}
return false;
});
if (currentItemIndex === -1) {
return {sidebarName, next: undefined, previous: undefined};
}
const {previous, next} = getElementsAround(
navigationItems,
currentItemIndex,
);
return {sidebarName, previous, next};
} else {
return emptySidebarNavigation(); return emptySidebarNavigation();
} }
if (!sidebarNameToNavigationItems[sidebarName]) {
throw new Error(
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
);
}
const navigationItems = sidebarNameToNavigationItems[sidebarName];
const currentItemIndex = navigationItems.findIndex((item) => {
if (item.type === 'doc') {
return item.id === docId;
}
if (item.type === 'category' && item.link.type === 'doc') {
return item.link.id === docId;
}
return false;
});
if (currentItemIndex === -1) {
return {sidebarName, next: undefined, previous: undefined};
}
const {previous, next} = getElementsAround(
navigationItems,
currentItemIndex,
);
return {sidebarName, previous, next};
} }
function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] { function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] {
@ -237,8 +238,10 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
}); });
} }
// We identity the category generated index by its permalink (should be unique) /**
// More reliable than using object identity * We identity the category generated index by its permalink (should be
* unique). More reliable than using object identity
*/
function getCategoryGeneratedIndexNavigation( function getCategoryGeneratedIndexNavigation(
categoryGeneratedIndexPermalink: string, categoryGeneratedIndexPermalink: string,
): SidebarNavigation { ): SidebarNavigation {
@ -257,19 +260,18 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
navigationItems.find(isCurrentCategoryGeneratedIndexItem), navigationItems.find(isCurrentCategoryGeneratedIndexItem),
)?.[0]; )?.[0];
if (sidebarName) { if (!sidebarName) {
const navigationItems = sidebarNameToNavigationItems[sidebarName];
const currentItemIndex = navigationItems.findIndex(
isCurrentCategoryGeneratedIndexItem,
);
const {previous, next} = getElementsAround(
navigationItems,
currentItemIndex,
);
return {sidebarName, previous, next};
} else {
return emptySidebarNavigation(); return emptySidebarNavigation();
} }
const navigationItems = sidebarNameToNavigationItems[sidebarName];
const currentItemIndex = navigationItems.findIndex(
isCurrentCategoryGeneratedIndexItem,
);
const {previous, next} = getElementsAround(
navigationItems,
currentItemIndex,
);
return {sidebarName, previous, next};
} }
function checkSidebarsDocIds(validDocIds: string[], sidebarFilePath: string) { function checkSidebarsDocIds(validDocIds: string[], sidebarFilePath: string) {
@ -322,11 +324,10 @@ Available document ids are:
slug: item.link.slug, slug: item.link.slug,
label: item.label, label: item.label,
}; };
} else { }
const firstSubItem = getFirstLink(item.items); const firstSubItem = getFirstLink(item.items);
if (firstSubItem) { if (firstSubItem) {
return firstSubItem; return firstSubItem;
}
} }
} }
} }
@ -371,18 +372,6 @@ export function toNavigationLink(
return doc; return doc;
} }
function handleCategory(category: SidebarItemCategoryWithLink): DocNavLink {
if (category.link.type === 'doc') {
return toDocNavigationLink(getDocById(category.link.id));
} else if (category.link.type === 'generated-index') {
return {
title: category.label,
permalink: category.link.permalink,
};
} else {
throw new Error('unexpected category link type');
}
}
if (!navigationItem) { if (!navigationItem) {
return undefined; return undefined;
} }
@ -390,8 +379,15 @@ export function toNavigationLink(
if (navigationItem.type === 'doc') { if (navigationItem.type === 'doc') {
return toDocNavigationLink(getDocById(navigationItem.id)); return toDocNavigationLink(getDocById(navigationItem.id));
} else if (navigationItem.type === 'category') { } else if (navigationItem.type === 'category') {
return handleCategory(navigationItem); if (navigationItem.link.type === 'doc') {
} else { return toDocNavigationLink(getDocById(navigationItem.link.id));
throw new Error('unexpected navigation item'); } else if (navigationItem.link.type === 'generated-index') {
return {
title: navigationItem.label,
permalink: navigationItem.link.permalink,
};
}
throw new Error('unexpected category link type');
} }
throw new Error('unexpected navigation item');
} }

View file

@ -23,7 +23,8 @@ import {isCategoriesShorthand} from './utils';
import type {CategoryMetadataFile} from './generator'; import type {CategoryMetadataFile} from './generator';
// NOTE: we don't add any default values during validation on purpose! // NOTE: we don't add any default values during validation on purpose!
// Config types are exposed to users for typechecking and we use the same type in normalization // Config types are exposed to users for typechecking and we use the same type
// in normalization
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({ const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
className: Joi.string(), className: Joi.string(),
@ -71,7 +72,8 @@ const sidebarItemCategoryLinkSchema = Joi.object<SidebarItemCategoryLink>()
then: Joi.object<SidebarItemCategoryLinkGeneratedIndex>({ then: Joi.object<SidebarItemCategoryLinkGeneratedIndex>({
type: 'generated-index', type: 'generated-index',
slug: Joi.string().optional(), slug: Joi.string().optional(),
// permalink: Joi.string().optional(), // No, this one is not in the user config, only in the normalized version // This one is not in the user config, only in the normalized version
// permalink: Joi.string().optional(),
title: Joi.string().optional(), title: Joi.string().optional(),
description: Joi.string().optional(), description: Joi.string().optional(),
image: Joi.string().optional(), image: Joi.string().optional(),
@ -132,7 +134,8 @@ function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig {
return; return;
} }
// TODO: remove once with proper Joi support // TODO: remove once with proper Joi support
// Because we can't use Joi to validate nested items (see above), we do it manually // Because we can't use Joi to validate nested items (see above), we do it
// manually
if (isCategoriesShorthand(item as SidebarItemConfig)) { if (isCategoriesShorthand(item as SidebarItemConfig)) {
Object.values(item as SidebarCategoriesShorthand).forEach((category) => Object.values(item as SidebarCategoriesShorthand).forEach((category) =>
category.forEach(validateSidebarItem), category.forEach(validateSidebarItem),

View file

@ -48,17 +48,16 @@ export default function getSlug({
function computeSlug(): string { function computeSlug(): string {
if (frontMatterSlug?.startsWith('/')) { if (frontMatterSlug?.startsWith('/')) {
return frontMatterSlug; return frontMatterSlug;
} else {
const dirNameSlug = getDirNameSlug();
if (
!frontMatterSlug &&
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
) {
return dirNameSlug;
}
const baseSlug = frontMatterSlug || baseID;
return resolvePathname(baseSlug, getDirNameSlug());
} }
const dirNameSlug = getDirNameSlug();
if (
!frontMatterSlug &&
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
) {
return dirNameSlug;
}
const baseSlug = frontMatterSlug || baseID;
return resolvePathname(baseSlug, getDirNameSlug());
} }
function ensureValidSlug(slug: string): string { function ensureValidSlug(slug: string): string {

View file

@ -31,11 +31,10 @@ import {CURRENT_VERSION_NAME} from './constants';
function getVersionFileName(versionName: string): string { function getVersionFileName(versionName: string): string {
if (versionName === CURRENT_VERSION_NAME) { if (versionName === CURRENT_VERSION_NAME) {
return versionName; return versionName;
} else {
// I don't like this "version-" prefix,
// but it's for consistency with site/versioned_docs
return `version-${versionName}`;
} }
// I don't like this "version-" prefix,
// but it's for consistency with site/versioned_docs
return `version-${versionName}`;
} }
// TODO legacy, the sidebar name is like "version-2.0.0-alpha.66/docs" // TODO legacy, the sidebar name is like "version-2.0.0-alpha.66/docs"
@ -68,7 +67,8 @@ function getDocTranslations(doc: DocMetadata): TranslationFileContent {
? { ? {
[`${doc.unversionedId}.sidebar_label`]: { [`${doc.unversionedId}.sidebar_label`]: {
message: doc.sidebar_label, message: doc.sidebar_label,
description: `The sidebar label for doc with id=${doc.unversionedId}`, description:
`The sidebar label for doc with id=${doc.unversionedId}`,
}, },
} }
: undefined), : undefined),
@ -253,7 +253,8 @@ function getVersionTranslationFiles(version: LoadedVersion): TranslationFiles {
const sidebarsTranslations: TranslationFileContent = const sidebarsTranslations: TranslationFileContent =
getSidebarsTranslations(version); getSidebarsTranslations(version);
// const docsTranslations: TranslationFileContent = getDocsTranslations(version); // const docsTranslations: TranslationFileContent =
// getDocsTranslations(version);
return [ return [
{ {

View file

@ -33,11 +33,9 @@ import {resolveSidebarPathOption} from './sidebars';
// retro-compatibility: no prefix for the default plugin id // retro-compatibility: no prefix for the default plugin id
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string { function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
if (pluginId === DEFAULT_PLUGIN_ID) { return pluginId === DEFAULT_PLUGIN_ID
return fileOrDir; ? fileOrDir
} else { : `${pluginId}_${fileOrDir}`;
return `${pluginId}_${fileOrDir}`;
}
} }
export function getVersionedDocsDirPath( export function getVersionedDocsDirPath(
@ -96,9 +94,8 @@ async function readVersionsFile(
const content = JSON.parse(await fs.readFile(versionsFilePath, 'utf8')); const content = JSON.parse(await fs.readFile(versionsFilePath, 'utf8'));
ensureValidVersionArray(content); ensureValidVersionArray(content);
return content; return content;
} else {
return null;
} }
return null;
} }
async function readVersionNames( async function readVersionNames(
@ -274,15 +271,13 @@ function getDefaultVersionBanner({
return null; return null;
} }
// Upcoming versions: unreleased banner // Upcoming versions: unreleased banner
else if ( if (
versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName) versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
) { ) {
return 'unreleased'; return 'unreleased';
} }
// Older versions: display unmaintained banner // Older versions: display unmaintained banner
else { return 'unmaintained';
return 'unmaintained';
}
} }
function getVersionBanner({ function getVersionBanner({
@ -443,8 +438,9 @@ function checkVersionMetadataPaths({
); );
} }
// If the current version defines a path to a sidebar file that does not exist, we throw! // If the current version defines a path to a sidebar file that does not
// Note: for versioned sidebars, the file may not exist (as we prefer to not create it rather than to create an empty file) // exist, we throw! Note: for versioned sidebars, the file may not exist (as
// we prefer to not create it rather than to create an empty file)
// See https://github.com/facebook/docusaurus/issues/3366 // See https://github.com/facebook/docusaurus/issues/3366
// See https://github.com/facebook/docusaurus/pull/4775 // See https://github.com/facebook/docusaurus/pull/4775
if ( if (
@ -469,11 +465,10 @@ Please set the docs "sidebarPath" field in your config file to:
function getDefaultLastVersionName(versionNames: string[]) { function getDefaultLastVersionName(versionNames: string[]) {
if (versionNames.length === 1) { if (versionNames.length === 1) {
return versionNames[0]; return versionNames[0];
} else {
return versionNames.filter(
(versionName) => versionName !== CURRENT_VERSION_NAME,
)[0];
} }
return versionNames.filter(
(versionName) => versionName !== CURRENT_VERSION_NAME,
)[0];
} }
function checkVersionsOptions( function checkVersionsOptions(
@ -544,9 +539,8 @@ function filterVersions(
return versionNamesUnfiltered.filter((name) => return versionNamesUnfiltered.filter((name) =>
(options.onlyIncludeVersions || []).includes(name), (options.onlyIncludeVersions || []).includes(name),
); );
} else {
return versionNamesUnfiltered;
} }
return versionNamesUnfiltered;
} }
export async function readVersionsMetadata({ export async function readVersionsMetadata({

View file

@ -112,29 +112,28 @@ export default async function pluginContentPages(
options.routeBasePath, options.routeBasePath,
encodePath(fileToPath(relativeSource)), encodePath(fileToPath(relativeSource)),
]); ]);
if (isMarkdownSource(relativeSource)) { if (!isMarkdownSource(relativeSource)) {
const content = await fs.readFile(source, 'utf-8');
const {
frontMatter: unsafeFrontMatter,
contentTitle,
excerpt,
} = parseMarkdownString(content);
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
return {
type: 'mdx',
permalink,
source: aliasedSourcePath,
title: frontMatter.title ?? contentTitle,
description: frontMatter.description ?? excerpt,
frontMatter,
};
} else {
return { return {
type: 'jsx', type: 'jsx',
permalink, permalink,
source: aliasedSourcePath, source: aliasedSourcePath,
}; };
} }
const content = await fs.readFile(source, 'utf-8');
const {
frontMatter: unsafeFrontMatter,
contentTitle,
excerpt,
} = parseMarkdownString(content);
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
return {
type: 'mdx',
permalink,
source: aliasedSourcePath,
title: frontMatter.title ?? contentTitle,
description: frontMatter.description ?? excerpt,
frontMatter,
};
} }
return Promise.all(pagesFiles.map(toMetadata)); return Promise.all(pagesFiles.map(toMetadata));

View file

@ -33,6 +33,8 @@ declare module '@theme/DebugJsonView' {
} }
declare module '@theme/DebugLayout' { declare module '@theme/DebugLayout' {
import type {ReactNode} from 'react';
export default function DebugLayout(props: { export default function DebugLayout(props: {
children: ReactNode; children: ReactNode;
}): JSX.Element; }): JSX.Element;

View file

@ -7,7 +7,7 @@
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
export default (function () { export default (function analyticsModule() {
if (!ExecutionEnvironment.canUseDOM) { if (!ExecutionEnvironment.canUseDOM) {
return null; return null;
} }

View file

@ -9,7 +9,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import globalData from '@generated/globalData'; import globalData from '@generated/globalData';
import type {PluginOptions} from '@docusaurus/plugin-google-gtag'; import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
export default (function () { export default (function gtagModule() {
if (!ExecutionEnvironment.canUseDOM) { if (!ExecutionEnvironment.canUseDOM) {
return null; return null;
} }
@ -19,7 +19,8 @@ export default (function () {
return { return {
onRouteUpdate({location}: {location: Location}) { onRouteUpdate({location}: {location: Location}) {
// Always refer to the variable on window in-case it gets overridden elsewhere. // Always refer to the variable on window in case it gets overridden
// elsewhere.
window.gtag('config', trackingID, { window.gtag('config', trackingID, {
page_path: location.pathname, page_path: location.pathname,
page_title: document.title, page_title: document.title,

View file

@ -40,7 +40,8 @@ export default function pluginGoogleGtag(
return {}; return {};
} }
return { return {
// Gtag includes GA by default, so we also preconnect to google-analytics. // Gtag includes GA by default, so we also preconnect to
// google-analytics.
headTags: [ headTags: [
{ {
tagName: 'link', tagName: 'link',

View file

@ -9,9 +9,12 @@
/** /**
* @see https://github.com/endiliey/react-ideal-image/blob/master/index.d.ts * @see https://github.com/endiliey/react-ideal-image/blob/master/index.d.ts
* Note: the original type definition is WRONG. getIcon & getMessage receive full state object. * Note: the original type definition is WRONG. getIcon & getMessage receive
* full state object.
*/ */
declare module '@endiliey/react-ideal-image' { declare module '@endiliey/react-ideal-image' {
import type {ComponentProps, ComponentType, CSSProperties} from 'react';
export type LoadingState = 'initial' | 'loading' | 'loaded' | 'error'; export type LoadingState = 'initial' | 'loading' | 'loaded' | 'error';
export type State = { export type State = {
@ -39,19 +42,21 @@ declare module '@endiliey/react-ideal-image' {
type ThemeKey = 'placeholder' | 'img' | 'icon' | 'noscript'; type ThemeKey = 'placeholder' | 'img' | 'icon' | 'noscript';
export interface ImageProps { export interface ImageProps extends ComponentProps<'img'> {
/** /**
* This function decides what icon to show based on the current state of the component. * This function decides what icon to show based on the current state of the
* component.
*/ */
getIcon?: (state: State) => IconKey; getIcon?: (state: State) => IconKey;
/** /**
* This function decides what message to show based on the icon (returned from getIcon prop) and * This function decides what message to show based on the icon (returned
* the current state of the component. * from `getIcon` prop) and the current state of the component.
*/ */
getMessage?: (icon: IconKey, state: State) => string; getMessage?: (icon: IconKey, state: State) => string | null;
/** /**
* This function is called as soon as the component enters the viewport and is used to generate urls * This function is called as soon as the component enters the viewport and
* based on width and format if props.srcSet doesn't provide src field. * is used to generate urls based on width and format if `props.srcSet`
* doesn't provide `src` field.
*/ */
getUrl?: (srcType: SrcType) => string; getUrl?: (srcType: SrcType) => string;
/** /**
@ -59,10 +64,11 @@ declare module '@endiliey/react-ideal-image' {
*/ */
height: number; height: number;
/** /**
* This provides a map of the icons. By default, the component uses icons from material design, * This provides a map of the icons. By default, the component uses icons
* implemented as React components with the SVG element. You can customize icons * from material design, Implemented as React components with the SVG
* element. You can customize icons
*/ */
icons: Partial<Record<IconKey, ComponentType>>; icons?: Partial<Record<IconKey, ComponentType>>;
/** /**
* This prop takes one of the 2 options, xhr and image. * This prop takes one of the 2 options, xhr and image.
* Read more about it: * Read more about it:
@ -74,9 +80,10 @@ declare module '@endiliey/react-ideal-image' {
*/ */
placeholder: {color: string} | {lqip: string}; placeholder: {color: string} | {lqip: string};
/** /**
* This function decides if image should be downloaded automatically. The default function * This function decides if image should be downloaded automatically. The
* returns false for a 2g network, for a 3g network it decides based on props.threshold * default function returns false for a 2g network, for a 3g network it
* and for a 4g network it returns true by default. * decides based on `props.threshold` and for a 4g network it returns true
* by default.
*/ */
shouldAutoDownload?: (options: { shouldAutoDownload?: (options: {
connection?: 'slow-2g' | '2g' | '3g' | '4g'; connection?: 'slow-2g' | '2g' | '3g' | '4g';
@ -85,18 +92,20 @@ declare module '@endiliey/react-ideal-image' {
possiblySlowNetwork?: boolean; possiblySlowNetwork?: boolean;
}) => boolean; }) => boolean;
/** /**
* This provides an array of sources of different format and size of the image. * This provides an array of sources of different format and size of the
* Read more about it: * image. Read more about it:
* https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#srcset * https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#srcset
*/ */
srcSet: SrcType[]; srcSet: SrcType[];
/** /**
* This provides a theme to the component. By default, the component uses inline styles, * This provides a theme to the component. By default, the component uses
* but it is also possible to use CSS modules and override all styles. * inline styles, but it is also possible to use CSS modules and override
* all styles.
*/ */
theme?: Partial<Record<ThemeKey, string | CSSProperties>>; theme?: Partial<Record<ThemeKey, string | CSSProperties>>;
/** /**
* Tells how much to wait in milliseconds until consider the download to be slow. * Tells how much to wait in milliseconds until consider the download to be
* slow.
*/ */
threshold?: number; threshold?: number;
/** /**
@ -105,8 +114,6 @@ declare module '@endiliey/react-ideal-image' {
width: number; width: number;
} }
type IdealImageComponent = ComponentClass<ImageProps>; declare const IdealImage: (props: ImageProps) => JSX.Element;
declare const IdealImage: IdealImageComponent;
export default IdealImage; export default IdealImage;
} }

View file

@ -12,15 +12,21 @@ declare module '@docusaurus/plugin-ideal-image' {
*/ */
name?: string; name?: string;
/** /**
* Specify all widths you want to use; if a specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up). You may also declare a default sizes array in the loader options in your webpack.config.js. * Specify all widths you want to use; if a specified size exceeds the
* original image's width, the latter will be used (i.e. images won't be
* scaled up). You may also declare a default sizes array in the loader
* options in your webpack.config.js.
*/ */
sizes?: number[]; sizes?: number[];
/** /**
* Specify one width you want to use; if the specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up) * Specify one width you want to use; if the specified size exceeds the
* original image's width, the latter will be used (i.e. images won't be
* scaled up)
*/ */
size?: number; size?: number;
/** /**
* As an alternative to manually specifying sizes, you can specify min, max and steps, and the sizes will be generated for you. * As an alternative to manually specifying sizes, you can specify min, max
* and steps, and the sizes will be generated for you.
*/ */
min?: number; min?: number;
/** /**

View file

@ -68,13 +68,12 @@ const getMessage = (icon: IconKey, state: State) => {
message: '404. Image not found', message: '404. Image not found',
description: 'When the image is not found', description: 'When the image is not found',
}); });
} else {
return translate({
id: 'theme.IdealImageMessage.error',
message: 'Error. Click to reload',
description: 'When the image fails to load for unknown error',
});
} }
return translate({
id: 'theme.IdealImageMessage.error',
message: 'Error. Click to reload',
description: 'When the image fails to load for unknown error',
});
} }
default: default:
throw new Error(`Wrong icon: ${icon}`); throw new Error(`Wrong icon: ${icon}`);

View file

@ -46,7 +46,7 @@ function getSWBabelLoader() {
}; };
} }
export default function ( export default function pluginPWA(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<void> { ): Plugin<void> {

View file

@ -219,8 +219,8 @@ async function registerSW() {
} }
} }
// TODO these events still works in chrome but have been removed from the spec in 2019! // TODO these events still works in chrome but have been removed from the spec
// See https://github.com/w3c/manifest/pull/836 // in 2019! See https://github.com/w3c/manifest/pull/836
function addLegacyAppInstalledEventsListeners() { function addLegacyAppInstalledEventsListeners() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
if (debug) { if (debug) {
@ -248,7 +248,8 @@ function addLegacyAppInstalledEventsListeners() {
await clearRegistrations(); await clearRegistrations();
}); });
// TODO this event still works in chrome but has been removed from the spec in 2019!!! // TODO this event still works in chrome but has been removed from the spec
// in 2019!!!
window.addEventListener('beforeinstallprompt', async (event) => { window.addEventListener('beforeinstallprompt', async (event) => {
if (debug) { if (debug) {
console.log( console.log(
@ -256,7 +257,8 @@ function addLegacyAppInstalledEventsListeners() {
event, event,
); );
} }
// TODO instead of default browser install UI, show custom docusaurus prompt? // TODO instead of default browser install UI, show custom docusaurus
// prompt?
// event.preventDefault(); // event.preventDefault();
if (debug) { if (debug) {
console.log( console.log(
@ -273,7 +275,7 @@ function addLegacyAppInstalledEventsListeners() {
} }
// After uninstalling the app, if the user doesn't clear all data, then // After uninstalling the app, if the user doesn't clear all data, then
// the previous service worker will continue serving cached files. We // the previous service worker will continue serving cached files. We
// need to clear registrations and reload, otherwise the popup will show. // need to clear registrations and reload, otherwise the popup shows.
await clearRegistrations(); await clearRegistrations();
} }
}); });

View file

@ -18,10 +18,10 @@ function parseSwParams() {
return params; return params;
} }
// doc advise against dynamic imports in SW // doc advises against dynamic imports in SW
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports // https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
// https://twitter.com/sebastienlorber/status/1280155204575518720 // https://twitter.com/sebastienlorber/status/1280155204575518720
// but I think it's working fine as it's inlined by webpack, need to double check? // but looks it's working fine as it's inlined by webpack, need to double check
async function runSWCustomCode(params) { async function runSWCustomCode(params) {
if (process.env.PWA_SW_CUSTOM) { if (process.env.PWA_SW_CUSTOM) {
const customSW = await import(process.env.PWA_SW_CUSTOM); const customSW = await import(process.env.PWA_SW_CUSTOM);
@ -70,6 +70,7 @@ function getPossibleURLs(url) {
(async () => { (async () => {
const params = parseSwParams(); const params = parseSwParams();
// eslint-disable-next-line no-underscore-dangle
const precacheManifest = self.__WB_MANIFEST; const precacheManifest = self.__WB_MANIFEST;
const controller = new PrecacheController({ const controller = new PrecacheController({
fallbackToNetwork: true, // safer to turn this true? fallbackToNetwork: true, // safer to turn this true?

View file

@ -6,7 +6,7 @@
*/ */
import remark from 'remark'; import remark from 'remark';
// import from the transpiled lib because Babel can't transpile `export =` syntax // import from the transpiled lib because Babel can't transpile `export =`
// TODO change to `../index` after migrating to ESM // TODO change to `../index` after migrating to ESM
import npm2yarn from '../../lib/index'; import npm2yarn from '../../lib/index';
import vfile from 'to-vfile'; import vfile from 'to-vfile';

View file

@ -23,7 +23,7 @@ const ContextReplacementPlugin: typeof webpack.ContextReplacementPlugin =
requireFromDocusaurusCore('webpack/lib/ContextReplacementPlugin'); requireFromDocusaurusCore('webpack/lib/ContextReplacementPlugin');
// Need to be inlined to prevent dark mode FOUC // Need to be inlined to prevent dark mode FOUC
// Make sure that the 'storageKey' is the same as the one in `/theme/hooks/useTheme.js` // Make sure the key is the same as the one in `/theme/hooks/useTheme.js`
const ThemeStorageKey = 'theme'; const ThemeStorageKey = 'theme';
const noFlashColorMode = ({ const noFlashColorMode = ({
defaultMode, defaultMode,
@ -64,15 +64,17 @@ const noFlashColorMode = ({
} }
})();`; })();`;
// Duplicated constant. Unfortunately we can't import it from theme-common, as we need to support older nodejs versions without ESM support // Duplicated constant. Unfortunately we can't import it from theme-common, as
// we need to support older nodejs versions without ESM support
// TODO: import from theme-common once we only support Node.js with ESM support // TODO: import from theme-common once we only support Node.js with ESM support
// + move all those announcementBar stuff there too // + move all those announcementBar stuff there too
export const AnnouncementBarDismissStorageKey = export const AnnouncementBarDismissStorageKey =
'docusaurus.announcement.dismiss'; 'docusaurus.announcement.dismiss';
const AnnouncementBarDismissDataAttribute = const AnnouncementBarDismissDataAttribute =
'data-announcement-bar-initially-dismissed'; 'data-announcement-bar-initially-dismissed';
// We always render the announcement bar html on the server, to prevent layout shifts on React hydration // We always render the announcement bar html on the server, to prevent layout
// The theme can use CSS + the data attribute to hide the announcement bar asap (before React hydration) // shifts on React hydration. The theme can use CSS + the data attribute to hide
// the announcement bar asap (before React hydration)
const AnnouncementBarInlineJavaScript = ` const AnnouncementBarInlineJavaScript = `
(function() { (function() {
function isDismissed() { function isDismissed() {

View file

@ -22,7 +22,9 @@ const threshold = 300;
// TODO proper detection is currently unreliable! // TODO proper detection is currently unreliable!
// see https://github.com/wessberg/scroll-behavior-polyfill/issues/16 // see https://github.com/wessberg/scroll-behavior-polyfill/issues/16
const SupportsNativeSmoothScrolling = false; const SupportsNativeSmoothScrolling = false;
// const SupportsNativeSmoothScrolling = ExecutionEnvironment.canUseDOM && 'scrollBehavior' in document.documentElement.style; // const SupportsNativeSmoothScrolling =
// ExecutionEnvironment.canUseDOM &&
// 'scrollBehavior' in document.documentElement.style;
type CancelScrollTop = () => void; type CancelScrollTop = () => void;
@ -44,13 +46,14 @@ function smoothScrollTopPolyfill(): CancelScrollTop {
} }
rafRecursion(); rafRecursion();
// Break the recursion // Break the recursion. Prevents the user from "fighting" against that
// Prevents the user from "fighting" against that recursion producing a weird UX // recursion producing a weird UX
return () => raf && cancelAnimationFrame(raf); return () => raf && cancelAnimationFrame(raf);
} }
type UseSmoothScrollTopReturn = { type UseSmoothScrollTopReturn = {
// We use a cancel function because the non-native smooth scroll-top implementation must be interrupted if user scroll down // We use a cancel function because the non-native smooth scroll-top
// implementation must be interrupted if user scroll down
smoothScrollTop: () => void; smoothScrollTop: () => void;
cancelScrollToTop: CancelScrollTop; cancelScrollToTop: CancelScrollTop;
}; };

View file

@ -21,27 +21,24 @@ function BlogPostAuthor({author}: Props): JSX.Element {
</Link> </Link>
)} )}
{ {name && (
// Note: only legacy author front matter allow empty name (not frontMatter.authors) <div
name && ( className="avatar__intro"
<div itemProp="author"
className="avatar__intro" itemScope
itemProp="author" itemType="https://schema.org/Person">
itemScope <div className="avatar__name">
itemType="https://schema.org/Person"> <Link href={url} itemProp="url">
<div className="avatar__name"> <span itemProp="name">{name}</span>
<Link href={url} itemProp="url"> </Link>
<span itemProp="name">{name}</span>
</Link>
</div>
{title && (
<small className="avatar__subtitle" itemProp="description">
{title}
</small>
)}
</div> </div>
) {title && (
} <small className="avatar__subtitle" itemProp="description">
{title}
</small>
)}
</div>
)}
</div> </div>
); );
} }

View file

@ -53,8 +53,9 @@ function BlogPostPage(props: Props): JSX.Element {
) : undefined ) : undefined
}> }>
<Seo <Seo
// TODO refactor needed: it's a bit annoying but Seo MUST be inside BlogLayout // TODO refactor needed: it's a bit annoying but Seo MUST be inside
// otherwise default image (set by BlogLayout) would shadow the custom blog post image // BlogLayout, otherwise default image (set by BlogLayout) would shadow
// the custom blog post image
title={title} title={title}
description={description} description={description}
keywords={keywords} keywords={keywords}

View file

@ -51,7 +51,8 @@ export default function CodeBlock({
const prismTheme = usePrismTheme(); const prismTheme = usePrismTheme();
// <pre> tags in markdown map to CodeBlocks and they may contain JSX children. // <pre> tags in markdown map to CodeBlocks and they may contain JSX children.
// When the children is not a simple string, we just return a styled block without actually highlighting. // When the children is not a simple string, we just return a styled block
// without actually highlighting.
if (React.Children.toArray(children).some((el) => isValidElement(el))) { if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return ( return (
<Highlight <Highlight

View file

@ -11,7 +11,8 @@ import {Details as DetailsGeneric} from '@docusaurus/theme-common';
import type {Props} from '@theme/Details'; import type {Props} from '@theme/Details';
import styles from './styles.module.css'; import styles from './styles.module.css';
// Should we have a custom details/summary comp in Infima instead of reusing alert classes? // Should we have a custom details/summary comp in Infima instead of reusing
// alert classes?
const InfimaClasses = 'alert alert--info'; const InfimaClasses = 'alert alert--info';
export default function Details({...props}: Props): JSX.Element { export default function Details({...props}: Props): JSX.Element {

View file

@ -75,8 +75,9 @@ export default function DocItem(props: Props): JSX.Element {
<div <div
className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}> className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
{/* {/*
Title can be declared inside md content or declared through front matter and added manually Title can be declared inside md content or declared through
To make both cases consistent, the added title is added under the same div.markdown block front matter and added manually. To make both cases consistent,
the added title is added under the same div.markdown block
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120 See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
*/} */}
{shouldAddTitle && ( {shouldAddTitle && (

View file

@ -168,7 +168,7 @@ function DocPage(props: Props): JSX.Element {
return ( return (
<> <>
<Head> <Head>
{/* TODO we should add a core addRoute({htmlClassName}) generic plugin option */} {/* TODO we should add a core addRoute({htmlClassName}) action */}
<html className={versionMetadata.className} /> <html className={versionMetadata.className} />
</Head> </Head>
<DocsVersionProvider version={versionMetadata}> <DocsVersionProvider version={versionMetadata}>

View file

@ -48,7 +48,8 @@ export default function DocSidebarItem({
} }
} }
// If we navigate to a category and it becomes active, it should automatically expand itself // If we navigate to a category and it becomes active, it should automatically
// expand itself
function useAutoExpandActiveCategory({ function useAutoExpandActiveCategory({
isActive, isActive,
collapsed, collapsed,
@ -67,11 +68,14 @@ function useAutoExpandActiveCategory({
}, [isActive, wasActive, collapsed, setCollapsed]); }, [isActive, wasActive, collapsed, setCollapsed]);
} }
// When a collapsible category has no link, we still link it to its first child during SSR as a temporary fallback /**
// This allows to be able to navigate inside the category even when JS fails to load, is delayed or simply disabled * When a collapsible category has no link, we still link it to its first child
// React hydration becomes an optional progressive enhancement * during SSR as a temporary fallback. This allows to be able to navigate inside
// see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188 * the category even when JS fails to load, is delayed or simply disabled
// see https://github.com/facebook/docusaurus/issues/3030 * React hydration becomes an optional progressive enhancement
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
* see https://github.com/facebook/docusaurus/issues/3030
*/
function useCategoryHrefWithSSRFallback( function useCategoryHrefWithSSRFallback(
item: PropSidebarItemCategory, item: PropSidebarItemCategory,
): string | undefined { ): string | undefined {

View file

@ -19,8 +19,8 @@ import type {MDXComponentsObject} from '@theme/MDXComponents';
import './styles.css'; import './styles.css';
// MDX elements are wrapped through the MDX pragma // MDX elements are wrapped through the MDX pragma. In some cases (notably usage
// In some cases (notably usage with Head/Helmet) we need to unwrap those elements. // with Head/Helmet) we need to unwrap those elements.
function unwrapMDXElement(element: ReactElement) { function unwrapMDXElement(element: ReactElement) {
if (element?.props?.mdxType && element?.props?.originalType) { if (element?.props?.mdxType && element?.props?.originalType) {
const {mdxType, originalType, ...newProps} = element.props; const {mdxType, originalType, ...newProps} = element.props;
@ -55,7 +55,8 @@ const MDXComponents: MDXComponentsObject = {
), ),
details: (props): JSX.Element => { details: (props): JSX.Element => {
const items = React.Children.toArray(props.children) as ReactElement[]; const items = React.Children.toArray(props.children) as ReactElement[];
// Split summary item from the rest to pass it as a separate prop to the Details theme component // Split summary item from the rest to pass it as a separate prop to the
// Details theme component
const summary: ReactElement<ComponentProps<'summary'>> = items.find( const summary: ReactElement<ComponentProps<'summary'>> = items.find(
(item) => item?.props?.mdxType === 'summary', (item) => item?.props?.mdxType === 'summary',
)!; )!;

View file

@ -78,8 +78,8 @@ export default function DocsVersionDropdownNavbarItem({
? undefined ? undefined
: getVersionMainDoc(dropdownVersion).path; : getVersionMainDoc(dropdownVersion).path;
// We don't want to render a version dropdown with 0 or 1 item // We don't want to render a version dropdown with 0 or 1 item. If we build
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0']) // the site with a single docs version (onlyIncludeVersions: ['1.0.0']),
// We'd rather render a button instead of a dropdown // We'd rather render a button instead of a dropdown
if (items.length <= 1) { if (items.length <= 1) {
return ( return (

View file

@ -25,8 +25,8 @@ const NavbarItemComponents: Record<
search: () => SearchNavbarItem, search: () => SearchNavbarItem,
dropdown: () => DropdownNavbarItem, dropdown: () => DropdownNavbarItem,
// Need to lazy load these items as we don't know for sure the docs plugin is loaded // Need to lazy load these items as we don't know for sure the docs plugin is
// See https://github.com/facebook/docusaurus/issues/3360 // loaded. See https://github.com/facebook/docusaurus/issues/3360
/* eslint-disable @typescript-eslint/no-var-requires, global-require */ /* eslint-disable @typescript-eslint/no-var-requires, global-require */
docsVersion: () => require('@theme/NavbarItem/DocsVersionNavbarItem').default, docsVersion: () => require('@theme/NavbarItem/DocsVersionNavbarItem').default,
docsVersionDropdown: () => docsVersionDropdown: () =>

View file

@ -6,7 +6,7 @@
*/ */
// By default, the classic theme does not provide any SearchBar implementation // By default, the classic theme does not provide any SearchBar implementation
// If you swizzled this file, it is your responsibility to provide an implementation // If you swizzled this, it is your responsibility to provide an implementation
// Tip: swizzle the SearchBar from the Algolia theme for inspiration: // Tip: swizzle the SearchBar from the Algolia theme for inspiration:
// npm run swizzle @docusaurus/theme-search-algolia SearchBar // npm run swizzle @docusaurus/theme-search-algolia SearchBar
export {default} from '@docusaurus/Noop'; export {default} from '@docusaurus/Noop';

View file

@ -12,7 +12,7 @@ import TOCItems from '@theme/TOCItems';
import styles from './styles.module.css'; import styles from './styles.module.css';
// Using a custom className // Using a custom className
// This prevents TOC highlighting to highlight TOCInline/TOCCollapsible by mistake // This prevents TOCInline/TOCCollapsible getting highlighted by mistake
const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight'; const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight';
const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active'; const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active';

View file

@ -55,7 +55,7 @@ function TabsComponent(props: Props): JSX.Element {
}); });
const values = const values =
valuesProp ?? valuesProp ??
// We only pick keys that we recognize. MDX would inject some keys by default // Only pick keys that we recognize. MDX would inject some keys by default
children.map(({props: {value, label, attributes}}) => ({ children.map(({props: {value, label, attributes}}) => ({
value, value,
label, label,

View file

@ -171,7 +171,7 @@ export function translateThemeConfig({
themeConfig, themeConfig,
translationFiles, translationFiles,
}: { }: {
// Why partial? To make TS correctly figure out the contravariance in parameter. // To make TS correctly figure out the contravariance in parameter.
// In practice it's always normalized // In practice it's always normalized
themeConfig: ThemeConfig; themeConfig: ThemeConfig;
translationFiles: TranslationFile[]; translationFiles: TranslationFile[];

View file

@ -54,8 +54,8 @@ const NavbarItemBaseSchema = Joi.object({
label: Joi.string(), label: Joi.string(),
className: Joi.string(), className: Joi.string(),
}) })
// We allow any unknown attributes on the links // We allow any unknown attributes on the links (users may need additional
// (users may need additional attributes like target, aria-role, data-customAttribute...) // attributes like target, aria-role, data-customAttribute...)
.unknown(); .unknown();
const DefaultNavbarItemSchema = NavbarItemBaseSchema.append({ const DefaultNavbarItemSchema = NavbarItemBaseSchema.append({
@ -251,8 +251,8 @@ const FooterLinkItemSchema = Joi.object({
.with('to', 'label') .with('to', 'label')
.with('href', 'label') .with('href', 'label')
.nand('html', 'label') .nand('html', 'label')
// We allow any unknown attributes on the links // We allow any unknown attributes on the links (users may need additional
// (users may need additional attributes like target, aria-role, data-customAttribute...) // attributes like target, aria-role, data-customAttribute...)
.unknown(); .unknown();
const CustomCssSchema = Joi.alternatives() const CustomCssSchema = Joi.alternatives()

View file

@ -67,7 +67,8 @@ function applyCollapsedStyle(el: HTMLElement, collapsed: boolean) {
} }
/* /*
Lex111: Dynamic transition duration is used in Material design, this technique is good for a large number of items. Lex111: Dynamic transition duration is used in Material design, this technique
is good for a large number of items.
https://material.io/archive/guidelines/motion/duration-easing.html#duration-easing-dynamic-durations https://material.io/archive/guidelines/motion/duration-easing.html#duration-easing-dynamic-durations
https://github.com/mui-org/material-ui/blob/e724d98eba018e55e1a684236a2037e24bcf050c/packages/material-ui/src/styles/createTransitions.js#L40-L43 https://github.com/mui-org/material-ui/blob/e724d98eba018e55e1a684236a2037e24bcf050c/packages/material-ui/src/styles/createTransitions.js#L40-L43
*/ */
@ -151,7 +152,8 @@ type CollapsibleElementType = React.ElementType<
Pick<React.HTMLAttributes<unknown>, 'className' | 'onTransitionEnd' | 'style'> Pick<React.HTMLAttributes<unknown>, 'className' | 'onTransitionEnd' | 'style'>
>; >;
// Prevent hydration layout shift before animations are handled imperatively with JS // Prevent hydration layout shift before animations are handled imperatively
// with JS
function getSSRStyle(collapsed: boolean) { function getSSRStyle(collapsed: boolean) {
if (ExecutionEnvironment.canUseDOM) { if (ExecutionEnvironment.canUseDOM) {
return undefined; return undefined;
@ -167,8 +169,9 @@ type CollapsibleBaseProps = {
onCollapseTransitionEnd?: (collapsed: boolean) => void; onCollapseTransitionEnd?: (collapsed: boolean) => void;
className?: string; className?: string;
// This is mostly useful for details/summary component where ssrStyle is not needed (as details are hidden natively) // This is mostly useful for details/summary component where ssrStyle is not
// and can mess-up with the default native behavior of the browser when JS fails to load or is disabled // needed (as details are hidden natively) and can mess up with the default
// native behavior of the browser when JS fails to load or is disabled
disableSSRStyle?: boolean; disableSSRStyle?: boolean;
}; };
@ -189,7 +192,8 @@ function CollapsibleBase({
return ( return (
<As <As
// @ts-expect-error: the "too complicated type" is produced from "CollapsibleElementType" being a huge union // @ts-expect-error: the "too complicated type" is produced from
// "CollapsibleElementType" being a huge union
ref={collapsibleRef} ref={collapsibleRef}
style={disableSSRStyle ? undefined : getSSRStyle(collapsed)} style={disableSSRStyle ? undefined : getSSRStyle(collapsed)}
onTransitionEnd={(e: React.TransitionEvent) => { onTransitionEnd={(e: React.TransitionEvent) => {
@ -215,7 +219,7 @@ function CollapsibleLazy({collapsed, ...props}: CollapsibleBaseProps) {
} }
}, [collapsed]); }, [collapsed]);
// lazyCollapsed updated in effect so that the first expansion transition can work // lazyCollapsed updated in effect so that first expansion transition can work
const [lazyCollapsed, setLazyCollapsed] = useState(collapsed); const [lazyCollapsed, setLazyCollapsed] = useState(collapsed);
useLayoutEffect(() => { useLayoutEffect(() => {
if (mounted) { if (mounted) {
@ -229,9 +233,10 @@ function CollapsibleLazy({collapsed, ...props}: CollapsibleBaseProps) {
} }
type CollapsibleProps = CollapsibleBaseProps & { type CollapsibleProps = CollapsibleBaseProps & {
// Lazy allows to delay the rendering when collapsed => it will render children only after hydration, on first expansion // Lazy allows to delay the rendering when collapsed => it will render
// Required prop: it forces to think if content should be server-rendered or not! // children only after hydration, on first expansion
// This has perf impact on the SSR output and html file sizes // Required prop: it forces to think if content should be server-rendered
// or not! This has perf impact on the SSR output and html file sizes
// See https://github.com/facebook/docusaurus/issues/4753 // See https://github.com/facebook/docusaurus/issues/4753
lazy: boolean; lazy: boolean;
}; };

View file

@ -41,7 +41,7 @@ function Details({summary, children, ...props}: DetailsProps): JSX.Element {
const {collapsed, setCollapsed} = useCollapsible({ const {collapsed, setCollapsed} = useCollapsible({
initialState: !props.open, initialState: !props.open,
}); });
// We use a separate prop because it must be set only after animation completes // Use a separate prop because it must be set only after animation completes
// Otherwise close anim won't work // Otherwise close anim won't work
const [open, setOpen] = useState(props.open); const [open, setOpen] = useState(props.open);

View file

@ -15,7 +15,8 @@ const windowSizes = {
// This "ssr" value is very important to handle hydration FOUC / layout shifts // This "ssr" value is very important to handle hydration FOUC / layout shifts
// You have to handle server-rendering explicitly on the call-site // You have to handle server-rendering explicitly on the call-site
// On the server, you may need to render BOTH the mobile/desktop elements (and hide one of them with mediaquery) // On the server, you may need to render BOTH the mobile/desktop elements (and
// hide one of them with mediaquery)
// We don't return "undefined" on purpose, to make it more explicit // We don't return "undefined" on purpose, to make it more explicit
ssr: 'ssr', ssr: 'ssr',
} as const; } as const;
@ -33,7 +34,8 @@ function getWindowSize() {
: windowSizes.mobile; : windowSizes.mobile;
} }
// Simulate the SSR window size in dev, so that potential hydration FOUC/layout shift problems can be seen in dev too! // Simulate the SSR window size in dev, so that potential hydration FOUC/layout
// shift problems can be seen in dev too!
const DevSimulateSSR = process.env.NODE_ENV === 'development' && true; const DevSimulateSSR = process.env.NODE_ENV === 'development' && true;
// This hook returns an enum value on purpose! // This hook returns an enum value on purpose!

View file

@ -7,7 +7,8 @@
// These class names are used to style page layouts in Docusaurus // These class names are used to style page layouts in Docusaurus
// Those are meant to be targeted by user-provided custom CSS selectors // Those are meant to be targeted by user-provided custom CSS selectors
// /!\ Please do not modify the classnames! This is a breaking change, and annoying for users! // Please do not modify the classnames! This is a breaking change, and annoying
// for users!
export const ThemeClassNames = { export const ThemeClassNames = {
page: { page: {
blogListPage: 'blog-list-page', blogListPage: 'blog-list-page',

View file

@ -112,8 +112,9 @@ describe('filterTOC', () => {
]); ]);
}); });
// It's not 100% clear exactly how the TOC should behave under weird heading levels provided by the user // It's not 100% clear exactly how the TOC should behave under weird heading
// Adding a test so that behavior stays the same over time // levels provided by the user. Adding a test so that behavior stays the same
// over time
test('filter invalid heading levels (but possible) TOC', () => { test('filter invalid heading levels (but possible) TOC', () => {
const toc: TOCItem[] = [ const toc: TOCItem[] = [
{ {

View file

@ -76,10 +76,9 @@ function readStorageState({
); );
if (versionExists) { if (versionExists) {
return {preferredVersionName: preferredVersionNameUnsafe}; return {preferredVersionName: preferredVersionNameUnsafe};
} else {
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
return {preferredVersionName: null};
} }
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
return {preferredVersionName: null};
} }
const initialState: DocsPreferredVersionState = {}; const initialState: DocsPreferredVersionState = {};
@ -144,9 +143,8 @@ export function DocsPreferredVersionContextProvider({
{children} {children}
</DocsPreferredVersionContextProviderUnsafe> </DocsPreferredVersionContextProviderUnsafe>
); );
} else {
return children;
} }
return children;
} }
function DocsPreferredVersionContextProviderUnsafe({ function DocsPreferredVersionContextProviderUnsafe({

View file

@ -20,7 +20,7 @@ import {useLocation} from '@docusaurus/router';
// TODO not ideal, see also "useDocs" // TODO not ideal, see also "useDocs"
export const isDocsPluginEnabled: boolean = !!useAllDocsData; export const isDocsPluginEnabled: boolean = !!useAllDocsData;
// Using a Symbol because null is a valid context value (a doc can have no sidebar) // Using a Symbol because null is a valid context value (a doc with no sidebar)
// Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx // Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx
const EmptyContextValue: unique symbol = Symbol('EmptyContext'); const EmptyContextValue: unique symbol = Symbol('EmptyContext');
@ -101,11 +101,10 @@ export function findSidebarCategory(
if (item.type === 'category') { if (item.type === 'category') {
if (predicate(item)) { if (predicate(item)) {
return item; return item;
} else { }
const subItem = findSidebarCategory(item.items, predicate); const subItem = findSidebarCategory(item.items, predicate);
if (subItem) { if (subItem) {
return subItem; return subItem;
}
} }
} }
} }

View file

@ -11,9 +11,10 @@ import type {Location, Action} from '@docusaurus/history';
type HistoryBlockHandler = (location: Location, action: Action) => void | false; type HistoryBlockHandler = (location: Location, action: Action) => void | false;
/* /**
Permits to register a handler that will be called on history actions (pop,push,replace) * Permits to register a handler that will be called on history actions (pop,
If the handler returns false, the navigation transition will be blocked/cancelled * push, replace) If the handler returns false, the navigation transition will
* be blocked/cancelled
*/ */
export function useHistoryActionHandler(handler: HistoryBlockHandler): void { export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
const {block} = useHistory(); const {block} = useHistory();
@ -32,11 +33,11 @@ export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
); );
} }
/* /**
Permits to register a handler that will be called on history pop navigation (backward/forward) * Permits to register a handler that will be called on history pop navigation
If the handler returns false, the backward/forward transition will be blocked * (backward/forward) If the handler returns false, the backward/forward
* transition will be blocked. Unfortunately there's no good way to detect the
Unfortunately there's no good way to detect the "direction" (backward/forward) of the POP event. * "direction" (backward/forward) of the POP event.
*/ */
export function useHistoryPopHandler(handler: HistoryBlockHandler): void { export function useHistoryPopHandler(handler: HistoryBlockHandler): void {
useHistoryActionHandler((location, action) => { useHistoryActionHandler((location, action) => {

View file

@ -10,8 +10,11 @@
/** /**
* Gets the duplicate values in an array. * Gets the duplicate values in an array.
* @param arr The array. * @param arr The array.
* @param comparator Compares two values and returns `true` if they are equal (duplicated). * @param comparator Compares two values and returns `true` if they are equal
* @returns Value of the elements `v` that have a preceding element `u` where `comparator(u, v) === true`. Values within the returned array are not guaranteed to be unique. * (duplicated).
* @returns Value of the elements `v` that have a preceding element `u` where
* `comparator(u, v) === true`. Values within the returned array are not
* guaranteed to be unique.
*/ */
export function duplicates<T>( export function duplicates<T>(
arr: readonly T[], arr: readonly T[],

View file

@ -16,12 +16,13 @@ import React, {
} from 'react'; } from 'react';
/* /*
The idea behind all this is that a specific component must be able to fill a placeholder in the generic layout The idea behind all this is that a specific component must be able to fill a
The doc page should be able to fill the secondary menu of the main mobile navbar. placeholder in the generic layout. The doc page should be able to fill the
This permits to reduce coupling between the main layout and the specific page. secondary menu of the main mobile navbar. This permits to reduce coupling
between the main layout and the specific page.
This kind of feature is often called portal/teleport/gateway... various unmaintained React libs exist This kind of feature is often called portal/teleport/gateway... various
Most up-to-date one: https://github.com/gregberge/react-teleporter unmaintained React libs exist. Most up-to-date one: https://github.com/gregberge/react-teleporter
Not sure any of those is safe regarding concurrent mode. Not sure any of those is safe regarding concurrent mode.
*/ */

View file

@ -7,19 +7,27 @@
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react'; import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
// This hook is like useLayoutEffect, but without the SSR warning /**
// It seems hacky but it's used in many React libs (Redux, Formik...) * This hook is like useLayoutEffect, but without the SSR warning
// Also mentioned here: https://github.com/facebook/react/issues/16956 * It seems hacky but it's used in many React libs (Redux, Formik...)
// It is useful when you need to update a ref as soon as possible after a React render (before useEffect) * Also mentioned here: https://github.com/facebook/react/issues/16956
* It is useful when you need to update a ref as soon as possible after a React
* render (before `useEffect`)
*/
export const useIsomorphicLayoutEffect = export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect; typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// Permits to transform an unstable callback (like an arrow function provided as props) /**
// to a "stable" callback that is safe to use in a useEffect dependency array * Permits to transform an unstable callback (like an arrow function provided as
// Useful to avoid React stale closure problems + avoid useless effect re-executions * props) to a "stable" callback that is safe to use in a useEffect dependency
// * array. Useful to avoid React stale closure problems + avoid useless effect
// Workaround until the React team recommends a good solution, see https://github.com/facebook/react/issues/16956 * re-executions
// This generally works has some potential drawbacks, such as https://github.com/facebook/react/issues/16956#issuecomment-536636418 *
* Workaround until the React team recommends a good solution, see
* https://github.com/facebook/react/issues/16956
* This generally works but has some potential drawbacks, such as
* https://github.com/facebook/react/issues/16956#issuecomment-536636418
*/
export function useDynamicCallback<T extends (...args: never[]) => unknown>( export function useDynamicCallback<T extends (...args: never[]) => unknown>(
callback: T, callback: T,
): T { ): T {
@ -29,6 +37,7 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
ref.current = callback; ref.current = callback;
}, [callback]); }, [callback]);
// @ts-expect-error: TS is right that this callback may be a supertype of T, but good enough for our use // @ts-expect-error: TS is right that this callback may be a supertype of T,
// but good enough for our use
return useCallback<T>((...args) => ref.current(...args), []); return useCallback<T>((...args) => ref.current(...args), []);
} }

View file

@ -6,7 +6,7 @@
*/ */
/** /**
* Utility to convert an optional string into a Regex case insensitive and global * Converts an optional string into a Regex case insensitive and global
*/ */
export function isRegexpStringMatch( export function isRegexpStringMatch(
regexAsString?: string, regexAsString?: string,

View file

@ -11,8 +11,8 @@ export type StorageType = typeof StorageTypes[number];
const DefaultStorageType: StorageType = 'localStorage'; const DefaultStorageType: StorageType = 'localStorage';
// Will return null browser storage is unavailable (like running Docusaurus in iframe) // Will return null browser storage is unavailable (like running Docusaurus in
// See https://github.com/facebook/docusaurus/pull/4501 // iframe) See https://github.com/facebook/docusaurus/pull/4501
function getBrowserStorage( function getBrowserStorage(
storageType: StorageType = DefaultStorageType, storageType: StorageType = DefaultStorageType,
): Storage | null { ): Storage | null {
@ -23,13 +23,12 @@ function getBrowserStorage(
} }
if (storageType === 'none') { if (storageType === 'none') {
return null; return null;
} else { }
try { try {
return window[storageType]; return window[storageType];
} catch (e) { } catch (e) {
logOnceBrowserStorageNotAvailableWarning(e as Error); logOnceBrowserStorageNotAvailableWarning(e as Error);
return null; return null;
}
} }
} }
@ -79,10 +78,10 @@ Please only call storage APIs in effects and event handlers.`);
/** /**
* Creates an object for accessing a particular key in localStorage. * Creates an object for accessing a particular key in localStorage.
* The API is fail-safe, and usage of browser storage should be considered unreliable * The API is fail-safe, and usage of browser storage should be considered
* Local storage might simply be unavailable (iframe + browser security) or operations might fail individually * unreliable. Local storage might simply be unavailable (iframe + browser
* Please assume that using this API can be a NO-OP * security) or operations might fail individually. Please assume that using
* See also https://github.com/facebook/docusaurus/issues/6036 * this API can be a NO-OP. See also https://github.com/facebook/docusaurus/issues/6036
*/ */
export const createStorageSlot = ( export const createStorageSlot = (
key: string, key: string,

View file

@ -36,9 +36,8 @@ export function filterTOC({
children: filteredChildren, children: filteredChildren,
}, },
]; ];
} else {
return filteredChildren;
} }
return filteredChildren;
}); });
} }

View file

@ -39,14 +39,15 @@ export function useAlternatePageUtils(): {
: `${baseUrlUnlocalized}${locale}/`; : `${baseUrlUnlocalized}${locale}/`;
} }
// TODO support correct alternate url when localized site is deployed on another domain // TODO support correct alternate url when localized site is deployed on
// another domain
function createUrl({ function createUrl({
locale, locale,
fullyQualified, fullyQualified,
}: { }: {
locale: string; locale: string;
// For hreflang SEO headers, we need it to be fully qualified (full protocol/domain/path...) // For hreflang SEO headers, we need it to be fully qualified (full
// For locale dropdown, using a path is good enough // protocol/domain/path...) or locale dropdown, using a path is good enough
fullyQualified: boolean; fullyQualified: boolean;
}) { }) {
return `${fullyQualified ? url : ''}${getLocalizedBaseUrl( return `${fullyQualified ? url : ''}${getLocalizedBaseUrl(

View file

@ -18,8 +18,8 @@ export type useContextualSearchFiltersReturns = {
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
// Maybe users will want to use its own search engine solution // Algolia/DocSearch. Maybe users want to use their own search engine solution
export function useContextualSearchFilters(): useContextualSearchFiltersReturns { export function useContextualSearchFilters(): useContextualSearchFiltersReturns {
const {i18n} = useDocusaurusContext(); const {i18n} = useDocusaurusContext();
const allDocsData = useAllDocsData(); const allDocsData = useAllDocsData();

View file

@ -49,23 +49,29 @@ function createLocalePluralForms(locale: string): LocalePluralForms {
}; };
} }
// Poor man's PluralSelector implementation, using an english fallback. /**
// We want a lightweight, future-proof and good-enough solution. * Poor man's PluralSelector implementation, using an english fallback. We want
// We don't want a perfect and heavy solution. * a lightweight, future-proof and good-enough solution. We don't want a perfect
// * and heavy solution.
// Docusaurus classic theme has only 2 deeply nested labels requiring complex plural rules *
// We don't want to use Intl + PluralRules polyfills + full ICU syntax (react-intl) just for that. * Docusaurus classic theme has only 2 deeply nested labels requiring complex
// * plural rules. We don't want to use Intl + PluralRules polyfills + full ICU
// Notes: * syntax (react-intl) just for that.
// - 2021: 92+% Browsers support Intl.PluralRules, and support will increase in the future *
// - NodeJS >= 13 has full ICU support by default * Notes:
// - In case of "mismatch" between SSR and Browser ICU support, React keeps working! * - 2021: 92+% Browsers support Intl.PluralRules, and support will increase in
* the future
* - NodeJS >= 13 has full ICU support by default
* - In case of "mismatch" between SSR and Browser ICU support, React keeps
* working!
*/
function useLocalePluralForms(): LocalePluralForms { function useLocalePluralForms(): LocalePluralForms {
const { const {
i18n: {currentLocale}, i18n: {currentLocale},
} = useDocusaurusContext(); } = useDocusaurusContext();
return useMemo(() => { return useMemo(() => {
// @ts-expect-error checking Intl.PluralRules in case browser doesn't have it (e.g Safari 12-) // @ts-expect-error checking Intl.PluralRules in case browser doesn't
// have it (e.g Safari 12-)
if (Intl.PluralRules) { if (Intl.PluralRules) {
try { try {
return createLocalePluralForms(currentLocale); return createLocalePluralForms(currentLocale);
@ -94,17 +100,17 @@ function selectPluralMessage(
if (parts.length === 1) { if (parts.length === 1) {
return parts[0]; return parts[0];
} else {
if (parts.length > localePluralForms.pluralForms.length) {
console.error(
`For locale=${localePluralForms.locale}, a maximum of ${localePluralForms.pluralForms.length} plural forms are expected (${localePluralForms.pluralForms}), but the message contains ${parts.length} plural forms: ${pluralMessages} `,
);
}
const pluralForm = localePluralForms.select(count);
const pluralFormIndex = localePluralForms.pluralForms.indexOf(pluralForm);
// In case of not enough plural form messages, we take the last one (other) instead of returning undefined
return parts[Math.min(pluralFormIndex, parts.length - 1)];
} }
if (parts.length > localePluralForms.pluralForms.length) {
console.error(
`For locale=${localePluralForms.locale}, a maximum of ${localePluralForms.pluralForms.length} plural forms are expected (${localePluralForms.pluralForms}), but the message contains ${parts.length} plural forms: ${pluralMessages} `,
);
}
const pluralForm = localePluralForms.select(count);
const pluralFormIndex = localePluralForms.pluralForms.indexOf(pluralForm);
// In case of not enough plural form messages, we take the last one (other)
// instead of returning undefined
return parts[Math.min(pluralFormIndex, parts.length - 1)];
} }
export function usePluralForm(): { export function usePluralForm(): {

View file

@ -13,7 +13,8 @@ TODO make the hardcoded theme-classic classnames configurable
(or add them to ThemeClassNames?) (or add them to ThemeClassNames?)
*/ */
// If the anchor has no height and is just a "marker" in the dom; we'll use the parent (normally the link text) rect boundaries instead // If the anchor has no height and is just a "marker" in the dom; we'll use the
// parent (normally the link text) rect boundaries instead
function getVisibleBoundingClientRect(element: HTMLElement): DOMRect { function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const hasNoHeight = rect.top === rect.bottom; const hasNoHeight = rect.top === rect.bottom;
@ -23,8 +24,10 @@ function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
return rect; return rect;
} }
// Considering we divide viewport into 2 zones of each 50vh /**
// This returns true if an element is in the first zone (ie, appear in viewport, near the top) * Considering we divide viewport into 2 zones of each 50vh, this returns true
* if an element is in the first zone (ie, appear in viewport, near the top)
*/
function isInViewportTopHalf(boundingRect: DOMRect) { function isInViewportTopHalf(boundingRect: DOMRect) {
return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2; return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2;
} }
@ -54,9 +57,10 @@ function getActiveAnchor(
anchorTopOffset: number; anchorTopOffset: number;
}, },
): Element | null { ): Element | null {
// Naming is hard // Naming is hard: The "nextVisibleAnchor" is the first anchor that appear
// The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary // under the viewport top boundary. It does not mean this anchor is visible
// Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible // yet, but if user continues scrolling down, it will be the first to become
// visible
const nextVisibleAnchor = anchors.find((anchor) => { const nextVisibleAnchor = anchors.find((anchor) => {
const boundingRect = getVisibleBoundingClientRect(anchor); const boundingRect = getVisibleBoundingClientRect(anchor);
return boundingRect.top >= anchorTopOffset; return boundingRect.top >= anchorTopOffset;
@ -64,23 +68,22 @@ function getActiveAnchor(
if (nextVisibleAnchor) { if (nextVisibleAnchor) {
const boundingRect = getVisibleBoundingClientRect(nextVisibleAnchor); const boundingRect = getVisibleBoundingClientRect(nextVisibleAnchor);
// If anchor is in the top half of the viewport: it is the one we consider "active" // If anchor is in the top half of the viewport: it is the one we consider
// (unless it's too close to the top and and soon to be scrolled outside viewport) // "active" (unless it's too close to the top and and soon to be scrolled
// outside viewport)
if (isInViewportTopHalf(boundingRect)) { if (isInViewportTopHalf(boundingRect)) {
return nextVisibleAnchor; return nextVisibleAnchor;
} }
// If anchor is in the bottom half of the viewport, or under the viewport, we consider the active anchor is the previous one // If anchor is in the bottom half of the viewport, or under the viewport,
// This is because the main text appearing in the user screen mostly belong to the previous anchor // we consider the active anchor is the previous one. This is because the
else { // main text appearing in the user screen mostly belong to the previous
// Returns null for the first anchor, see https://github.com/facebook/docusaurus/issues/5318 // anchor. Returns null for the first anchor, see
return anchors[anchors.indexOf(nextVisibleAnchor) - 1] ?? null; // https://github.com/facebook/docusaurus/issues/5318
} return anchors[anchors.indexOf(nextVisibleAnchor) - 1] ?? null;
} }
// no anchor under viewport top? (ie we are at the bottom of the page) // no anchor under viewport top? (ie we are at the bottom of the page)
// => highlight the last anchor found // => highlight the last anchor found
else { return anchors[anchors.length - 1];
return anchors[anchors.length - 1];
}
} }
function getLinkAnchorValue(link: HTMLAnchorElement): string { function getLinkAnchorValue(link: HTMLAnchorElement): string {

View file

@ -168,7 +168,8 @@ function DocSearch({
const transformItems = useRef<DocSearchModalProps['transformItems']>( const transformItems = useRef<DocSearchModalProps['transformItems']>(
(items) => (items) =>
items.map((item) => { items.map((item) => {
// If Algolia contains a external domain, we should navigate without relative URL // If Algolia contains a external domain, we should navigate without
// relative URL
if (isRegexpStringMatch(externalUrlRegex, item.url)) { if (isRegexpStringMatch(externalUrlRegex, item.url)) {
return item; return item;
} }

View file

@ -12,8 +12,8 @@ import type {Props} from '@theme/SearchMetadata';
// Override default/agnostic SearchMetadata to use Algolia-specific metadata // Override default/agnostic SearchMetadata to use Algolia-specific metadata
function SearchMetadata({locale, version, tag}: Props): JSX.Element { function SearchMetadata({locale, version, tag}: Props): 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
// as the existing docsearch:language filter is afaik a regular string-based filter // docsearch:language filter is afaik a regular string-based filter
const language = locale; const language = locale;
return ( return (

View file

@ -8,7 +8,3 @@
/// <reference types="@docusaurus/module-type-aliases" /> /// <reference types="@docusaurus/module-type-aliases" />
/// <reference types="@docusaurus/theme-common" /> /// <reference types="@docusaurus/theme-common" />
/// <reference types="@docusaurus/theme-classic" /> /// <reference types="@docusaurus/theme-classic" />
export type FacetFilters = Required<
Required<DocSearchProps>['searchParameters']
>['facetFilters'];

View file

@ -20,8 +20,8 @@ describe('codeTranslationLocalesToTry', () => {
'fr-Latn', 'fr-Latn',
]); ]);
expect(codeTranslationLocalesToTry('fr-FR')).toEqual(['fr-FR', 'fr']); expect(codeTranslationLocalesToTry('fr-FR')).toEqual(['fr-FR', 'fr']);
// Note: "pt" is expanded into "pt-BR", not "pt-PT", as "pt-BR" is more widely used! // Note: "pt" is expanded into "pt-BR", not "pt-PT", as "pt-BR" is more
// See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783 // widely used! See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
expect(codeTranslationLocalesToTry('pt')).toEqual([ expect(codeTranslationLocalesToTry('pt')).toEqual([
'pt', 'pt',
'pt-BR', 'pt-BR',

View file

@ -15,8 +15,8 @@ function getDefaultLocalesDirPath(): string {
// Return an ordered list of locales we should try // Return an ordered list of locales we should try
export function codeTranslationLocalesToTry(locale: string): string[] { export function codeTranslationLocalesToTry(locale: string): string[] {
const intlLocale = new Intl.Locale(locale); const intlLocale = new Intl.Locale(locale);
// if locale is just a simple language like "pt", we want to fallback to pt-BR (not pt-PT!) // if locale is just a simple language like "pt", we want to fallback to pt-BR
// see https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783 // (not pt-PT!) See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
if (intlLocale.language === locale) { if (intlLocale.language === locale) {
const maximizedLocale = intlLocale.maximize(); // pt-Latn-BR` const maximizedLocale = intlLocale.maximize(); // pt-Latn-BR`
// ["pt","pt-BR"]; ["zh", "zh-Hans"] // ["pt","pt-BR"]; ["zh", "zh-Hans"]
@ -27,9 +27,7 @@ export function codeTranslationLocalesToTry(locale: string): string[] {
]; ];
} }
// if locale is like "pt-BR", we want to fallback to "pt" // if locale is like "pt-BR", we want to fallback to "pt"
else { return [locale, intlLocale.language!];
return [locale, intlLocale.language!];
}
} }
// Useful to implement getDefaultCodeTranslationMessages() in themes // Useful to implement getDefaultCodeTranslationMessages() in themes

View file

@ -12,8 +12,8 @@ import type Joi from 'joi';
import type {Overwrite, DeepPartial} from 'utility-types'; 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
// see https://github.com/survivejs/webpack-merge/issues/179 // before) See https://github.com/survivejs/webpack-merge/issues/179
type MergeStrategy = 'match' | 'merge' | 'append' | 'prepend' | 'replace'; type MergeStrategy = 'match' | 'merge' | 'append' | 'prepend' | 'replace';
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw'; export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
@ -30,7 +30,8 @@ export interface DocusaurusConfig {
tagline: string; tagline: string;
title: string; title: string;
url: string; url: string;
// trailingSlash undefined = legacy retrocompatible behavior => /file => /file/index.html // trailingSlash undefined = legacy retrocompatible behavior
// /file => /file/index.html
trailingSlash: boolean | undefined; trailingSlash: boolean | undefined;
i18n: I18nConfig; i18n: I18nConfig;
onBrokenLinks: ReportingSeverity; onBrokenLinks: ReportingSeverity;
@ -73,8 +74,8 @@ export interface DocusaurusConfig {
} }
// Docusaurus config, as provided by the user (partial/unnormalized) // Docusaurus config, as provided by the user (partial/unnormalized)
// This type is used to provide type-safety / IDE auto-complete on the config file // This type is used to provide type-safety / IDE auto-complete on the config
// See https://docusaurus.io/docs/typescript-support // file. See https://docusaurus.io/docs/typescript-support
export type Config = Overwrite< export type Config = Overwrite<
Partial<DocusaurusConfig>, Partial<DocusaurusConfig>,
{ {
@ -88,7 +89,8 @@ export type Config = Overwrite<
/** /**
* - `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.
* - `type: 'local'`, none of plugin's ancestor directory contains any package.json. * - `type: 'local'`, none of the plugin's ancestor directories contains a
* package.json.
* - `type: 'synthetic'`, docusaurus generated internal plugin. * - `type: 'synthetic'`, docusaurus generated internal plugin.
*/ */
export type DocusaurusPluginVersionInformation = export type DocusaurusPluginVersionInformation =
@ -259,7 +261,8 @@ export interface Plugin<Content = unknown> {
}) => Promise<void>; }) => Promise<void>;
routesLoaded?: (routes: RouteConfig[]) => void; // TODO remove soon, deprecated (alpha-60) routesLoaded?: (routes: RouteConfig[]) => void; // TODO remove soon, deprecated (alpha-60)
postBuild?: (props: Props & {content: Content}) => Promise<void>; postBuild?: (props: Props & {content: Content}) => Promise<void>;
// TODO refactor the configureWebpack API surface: use an object instead of multiple params (requires breaking change) // TODO refactor the configureWebpack API surface: use an object instead of
// multiple params (requires breaking change)
configureWebpack?: ( configureWebpack?: (
config: Configuration, config: Configuration,
isServer: boolean, isServer: boolean,

View file

@ -45,8 +45,8 @@ export default function applyTrailingSlash(
// Never transform '/' to '' // Never transform '/' to ''
// Never remove the baseUrl trailing slash! // Never remove the baseUrl trailing slash!
// If baseUrl = /myBase/, we want to emit /myBase/index.html and not /myBase.html ! // If baseUrl = /myBase/, we want to emit /myBase/index.html and not
// See https://github.com/facebook/docusaurus/issues/5077 // /myBase.html! See https://github.com/facebook/docusaurus/issues/5077
const shouldNotApply = pathname === '/' || pathname === baseUrl; const shouldNotApply = pathname === '/' || pathname === baseUrl;
const newPathname = shouldNotApply const newPathname = shouldNotApply

View file

@ -7,11 +7,6 @@
import Joi from './Joi'; import Joi from './Joi';
// Enhance the default Joi.string() type so that it can convert number to strings
// If user use front matter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
// see https://github.com/facebook/docusaurus/issues/4642
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
const JoiFrontMatterString: Joi.Extension = { const JoiFrontMatterString: Joi.Extension = {
type: 'string', type: 'string',
base: Joi.string(), base: Joi.string(),
@ -23,4 +18,12 @@ const JoiFrontMatterString: Joi.Extension = {
return {value}; return {value};
}, },
}; };
/**
* Enhance the default Joi.string() type so that it can convert number to
* strings. If user use front matter "tag: 2021", we shouldn't need to ask her
* to write "tag: '2021'". Also yaml tries to convert patterns like "2019-01-01"
* to dates automatically.
* @see https://github.com/facebook/docusaurus/issues/4642
* @see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
*/
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString); export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);

View file

@ -43,13 +43,9 @@ function testMarkdownPluginSchemas(schema: Joi.Schema) {
}); });
testOK(undefined); testOK(undefined);
testOK([function () {}]); testOK([() => {}]);
testOK([[function () {}, {attr: 'val'}]]); testOK([[() => {}, {attr: 'val'}]]);
testOK([ testOK([[() => {}, {attr: 'val'}], () => {}, [() => {}, {attr: 'val'}]]);
[function () {}, {attr: 'val'}],
function () {},
[function () {}, {attr: 'val'}],
]);
testFail(null); testFail(null);
testFail(false); testFail(false);
@ -58,8 +54,8 @@ function testMarkdownPluginSchemas(schema: Joi.Schema) {
testFail([false]); testFail([false]);
testFail([3]); testFail([3]);
testFail([[]]); testFail([[]]);
testFail([[function () {}, undefined]]); testFail([[() => {}, undefined]]);
testFail([[function () {}, true]]); testFail([[() => {}, true]]);
} }
describe('validation schemas', () => { describe('validation schemas', () => {

Some files were not shown because too many files have changed in this diff Show more