refactor(client-redirects): elaborate documentation, minor refactor (#7607)

This commit is contained in:
Joshua Chen 2022-06-13 22:04:39 +08:00 committed by GitHub
parent 27834dc23a
commit fb3138d722
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 103 additions and 109 deletions

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toRedirectFilesMetadata creates appropriate metadata for empty baseUrl: fileContent baseUrl=empty 1`] = ` exports[`toRedirectFiles creates appropriate metadata for empty baseUrl: fileContent baseUrl=empty 1`] = `
[ [
"<!DOCTYPE html> "<!DOCTYPE html>
<html> <html>
@ -16,7 +16,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata for empty baseUrl:
] ]
`; `;
exports[`toRedirectFilesMetadata creates appropriate metadata for root baseUrl: fileContent baseUrl=/ 1`] = ` exports[`toRedirectFiles creates appropriate metadata for root baseUrl: fileContent baseUrl=/ 1`] = `
[ [
"<!DOCTYPE html> "<!DOCTYPE html>
<html> <html>
@ -32,7 +32,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata for root baseUrl:
] ]
`; `;
exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=false: fileContent 1`] = ` exports[`toRedirectFiles creates appropriate metadata trailingSlash=false: fileContent 1`] = `
[ [
"<!DOCTYPE html> "<!DOCTYPE html>
<html> <html>
@ -70,7 +70,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=fals
] ]
`; `;
exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=true: fileContent 1`] = ` exports[`toRedirectFiles creates appropriate metadata trailingSlash=true: fileContent 1`] = `
[ [
"<!DOCTYPE html> "<!DOCTYPE html>
<html> <html>
@ -108,7 +108,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=true
] ]
`; `;
exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=undefined: fileContent 1`] = ` exports[`toRedirectFiles creates appropriate metadata trailingSlash=undefined: fileContent 1`] = `
[ [
"<!DOCTYPE html> "<!DOCTYPE html>
<html> <html>

View file

@ -9,7 +9,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import writeRedirectFiles, { import writeRedirectFiles, {
toRedirectFilesMetadata, toRedirectFiles,
createToUrl, createToUrl,
} from '../writeRedirectFiles'; } from '../writeRedirectFiles';
@ -42,14 +42,14 @@ describe('createToUrl', () => {
}); });
}); });
describe('toRedirectFilesMetadata', () => { describe('toRedirectFiles', () => {
it('creates appropriate metadata trailingSlash=undefined', () => { it('creates appropriate metadata trailingSlash=undefined', () => {
const pluginContext = { const pluginContext = {
outDir: '/tmp/someFixedOutDir', outDir: '/tmp/someFixedOutDir',
baseUrl: 'https://docusaurus.io', baseUrl: 'https://docusaurus.io',
}; };
const redirectFiles = toRedirectFilesMetadata( const redirectFiles = toRedirectFiles(
[ [
{from: '/abc.html', to: '/abc'}, {from: '/abc.html', to: '/abc'},
{from: '/def', to: '/def.html'}, {from: '/def', to: '/def.html'},
@ -76,7 +76,7 @@ describe('toRedirectFilesMetadata', () => {
baseUrl: 'https://docusaurus.io', baseUrl: 'https://docusaurus.io',
}; };
const redirectFiles = toRedirectFilesMetadata( const redirectFiles = toRedirectFiles(
[ [
{from: '/abc.html', to: '/abc'}, {from: '/abc.html', to: '/abc'},
{from: '/def', to: '/def.html'}, {from: '/def', to: '/def.html'},
@ -103,7 +103,7 @@ describe('toRedirectFilesMetadata', () => {
baseUrl: 'https://docusaurus.io', baseUrl: 'https://docusaurus.io',
}; };
const redirectFiles = toRedirectFilesMetadata( const redirectFiles = toRedirectFiles(
[ [
{from: '/abc.html', to: '/abc'}, {from: '/abc.html', to: '/abc'},
{from: '/def', to: '/def.html'}, {from: '/def', to: '/def.html'},
@ -132,7 +132,7 @@ describe('toRedirectFilesMetadata', () => {
outDir: '/tmp/someFixedOutDir', outDir: '/tmp/someFixedOutDir',
baseUrl: '/', baseUrl: '/',
}; };
const redirectFiles = toRedirectFilesMetadata( const redirectFiles = toRedirectFiles(
[{from: '/abc.html', to: '/abc'}], [{from: '/abc.html', to: '/abc'}],
pluginContext, pluginContext,
undefined, undefined,
@ -147,7 +147,7 @@ describe('toRedirectFilesMetadata', () => {
outDir: '/tmp/someFixedOutDir', outDir: '/tmp/someFixedOutDir',
baseUrl: '', baseUrl: '',
}; };
const redirectFiles = toRedirectFilesMetadata( const redirectFiles = toRedirectFiles(
[{from: '/abc.html', to: '/abc'}], [{from: '/abc.html', to: '/abc'}],
pluginContext, pluginContext,
undefined, undefined,

View file

@ -7,52 +7,57 @@
import _ from 'lodash'; import _ from 'lodash';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import { import {applyTrailingSlash} from '@docusaurus/utils-common';
applyTrailingSlash,
type ApplyTrailingSlashParams,
} from '@docusaurus/utils-common';
import { import {
createFromExtensionsRedirects, createFromExtensionsRedirects,
createToExtensionsRedirects, createToExtensionsRedirects,
} from './extensionRedirects'; } from './extensionRedirects';
import {validateRedirect} from './redirectValidation'; import {validateRedirect} from './redirectValidation';
import type {PluginOptions, RedirectOption} from './options'; import type {PluginOptions, RedirectOption} from './options';
import type {PluginContext, RedirectMetadata} from './types'; import type {PluginContext, RedirectItem} from './types';
export default function collectRedirects( export default function collectRedirects(
pluginContext: PluginContext, pluginContext: PluginContext,
trailingSlash: boolean | undefined, trailingSlash: boolean | undefined,
): RedirectMetadata[] { ): RedirectItem[] {
let redirects = doCollectRedirects(pluginContext); // For each plugin config option, create the appropriate redirects
const redirects = [
redirects = applyRedirectsTrailingSlash(redirects, { ...createFromExtensionsRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.fromExtensions,
),
...createToExtensionsRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.toExtensions,
),
...createRedirectsOptionRedirects(pluginContext.options.redirects),
...createCreateRedirectsOptionRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.createRedirects,
),
].map((redirect) => ({
...redirect,
// Given a redirect with `to: "/abc"` and `trailingSlash` enabled:
//
// - We don't want to reject `to: "/abc"`, as that unambiguously points to
// `/abc/` now;
// - We want to redirect `to: /abc/` without the user having to change all
// her redirect plugin options
//
// It should be easy to toggle `trailingSlash` option without having to
// change other configs
to: applyTrailingSlash(redirect.to, {
trailingSlash, trailingSlash,
baseUrl: pluginContext.baseUrl, baseUrl: pluginContext.baseUrl,
}); }),
}));
validateCollectedRedirects(redirects, pluginContext); validateCollectedRedirects(redirects, pluginContext);
return filterUnwantedRedirects(redirects, pluginContext); return filterUnwantedRedirects(redirects, pluginContext);
} }
// 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 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(
redirects: RedirectMetadata[],
params: ApplyTrailingSlashParams,
) {
return redirects.map((redirect) => ({
...redirect,
to: applyTrailingSlash(redirect.to, params),
}));
}
function validateCollectedRedirects( function validateCollectedRedirects(
redirects: RedirectMetadata[], redirects: RedirectItem[],
pluginContext: PluginContext, pluginContext: PluginContext,
) { ) {
const redirectValidationErrors = redirects const redirectValidationErrors = redirects
@ -89,9 +94,9 @@ Valid paths you can redirect to:
} }
function filterUnwantedRedirects( function filterUnwantedRedirects(
redirects: RedirectMetadata[], redirects: RedirectItem[],
pluginContext: PluginContext, pluginContext: PluginContext,
): RedirectMetadata[] { ): RedirectItem[] {
// We don't want to create the same redirect twice, since that would lead to // We don't want to create the same redirect twice, since that would lead to
// writing the same html redirection file twice. // writing the same html redirection file twice.
Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach( Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach(
@ -120,37 +125,15 @@ It is not possible to redirect the same pathname to multiple destinations: ${gro
); );
} }
// For each plugin config option, create the appropriate redirects
function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] {
return [
...createFromExtensionsRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.fromExtensions,
),
...createToExtensionsRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.toExtensions,
),
...createRedirectsOptionRedirects(pluginContext.options.redirects),
...createCreateRedirectsOptionRedirects(
pluginContext.relativeRoutesPaths,
pluginContext.options.createRedirects,
),
];
}
function createRedirectsOptionRedirects( function createRedirectsOptionRedirects(
redirectsOption: PluginOptions['redirects'], redirectsOption: PluginOptions['redirects'],
): RedirectMetadata[] { ): RedirectItem[] {
// For convenience, user can use a string or a string[] // For convenience, user can use a string or a string[]
function optionToRedirects(option: RedirectOption): RedirectMetadata[] { function optionToRedirects(option: RedirectOption): RedirectItem[] {
if (typeof option.from === 'string') { if (typeof option.from === 'string') {
return [{from: option.from, to: option.to}]; return [{from: option.from, to: option.to}];
} }
return option.from.map((from) => ({ return option.from.map((from) => ({from, to: option.to}));
from,
to: option.to,
}));
} }
return redirectsOption.flatMap(optionToRedirects); return redirectsOption.flatMap(optionToRedirects);
@ -160,17 +143,14 @@ function createRedirectsOptionRedirects(
function createCreateRedirectsOptionRedirects( function createCreateRedirectsOptionRedirects(
paths: string[], paths: string[],
createRedirects: PluginOptions['createRedirects'], createRedirects: PluginOptions['createRedirects'],
): RedirectMetadata[] { ): RedirectItem[] {
function createPathRedirects(path: string): RedirectMetadata[] { function createPathRedirects(path: string): RedirectItem[] {
const fromsMixed: string | string[] = createRedirects?.(path) ?? []; const fromsMixed: string | string[] = createRedirects?.(path) ?? [];
const froms: string[] = const froms: string[] =
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed; typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
return froms.map((from) => ({ return froms.map((from) => ({from, to: path}));
from,
to: path,
}));
} }
return paths.flatMap(createPathRedirects); return paths.flatMap(createPathRedirects);

View file

@ -10,7 +10,7 @@ import {
removeSuffix, removeSuffix,
removeTrailingSlash, removeTrailingSlash,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import type {RedirectMetadata} from './types'; import type {RedirectItem} from './types';
const ExtensionAdditionalMessage = const ExtensionAdditionalMessage =
'If the redirect extension system is not good enough for your use case, you can create redirects yourself with the "createRedirects" plugin option.'; 'If the redirect extension system is not good enough for your use case, you can create redirects yourself with the "createRedirects" plugin option.';
@ -40,23 +40,21 @@ const validateExtension = (ext: string) => {
const addLeadingDot = (extension: string) => `.${extension}`; const addLeadingDot = (extension: string) => `.${extension}`;
// Create new /path that redirects to existing an /path.html /**
* Create new `/path` that redirects to existing an `/path.html`
*/
export function createToExtensionsRedirects( export function createToExtensionsRedirects(
paths: string[], paths: string[],
extensions: string[], extensions: string[],
): RedirectMetadata[] { ): RedirectItem[] {
extensions.forEach(validateExtension); extensions.forEach(validateExtension);
const dottedExtensions = extensions.map(addLeadingDot); const dottedExtensions = extensions.map(addLeadingDot);
const createPathRedirects = (path: string): RedirectMetadata[] => { const createPathRedirects = (path: string): RedirectItem[] => {
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext)); const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
if (extensionFound) { if (extensionFound) {
const routePathWithoutExtension = removeSuffix(path, extensionFound); return [{from: removeSuffix(path, extensionFound), to: path}];
return [routePathWithoutExtension].map((from) => ({
from,
to: path,
}));
} }
return []; return [];
}; };
@ -64,12 +62,15 @@ export function createToExtensionsRedirects(
return paths.flatMap(createPathRedirects); return paths.flatMap(createPathRedirects);
} }
// Create new /path.html/index.html that redirects to existing an /path /**
// The filename pattern might look weird but it's on purpose (see https://github.com/facebook/docusaurus/issues/5055) * Create new `/path.html/index.html` that redirects to existing an `/path`
* The filename pattern might look weird but it's on purpose (see
* https://github.com/facebook/docusaurus/issues/5055)
*/
export function createFromExtensionsRedirects( export function createFromExtensionsRedirects(
paths: string[], paths: string[],
extensions: string[], extensions: string[],
): RedirectMetadata[] { ): RedirectItem[] {
extensions.forEach(validateExtension); extensions.forEach(validateExtension);
const dottedExtensions = extensions.map(addLeadingDot); const dottedExtensions = extensions.map(addLeadingDot);
@ -77,7 +78,7 @@ export function createFromExtensionsRedirects(
const alreadyEndsWithAnExtension = (str: string) => const alreadyEndsWithAnExtension = (str: string) =>
dottedExtensions.some((ext) => str.endsWith(ext)); dottedExtensions.some((ext) => str.endsWith(ext));
const createPathRedirects = (path: string): RedirectMetadata[] => { const createPathRedirects = (path: string): RedirectItem[] => {
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) { if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
return []; return [];
} }

View file

@ -8,11 +8,11 @@
import {removePrefix, addLeadingSlash} from '@docusaurus/utils'; import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
import collectRedirects from './collectRedirects'; import collectRedirects from './collectRedirects';
import writeRedirectFiles, { import writeRedirectFiles, {
toRedirectFilesMetadata, toRedirectFiles,
type RedirectFileMetadata, type RedirectFile,
} from './writeRedirectFiles'; } from './writeRedirectFiles';
import type {LoadContext, Plugin} from '@docusaurus/types'; import type {LoadContext, Plugin} from '@docusaurus/types';
import type {PluginContext, RedirectMetadata} from './types'; import type {PluginContext, RedirectItem} from './types';
import type {PluginOptions, Options} from './options'; import type {PluginOptions, Options} from './options';
export default function pluginClientRedirectsPages( export default function pluginClientRedirectsPages(
@ -33,12 +33,12 @@ export default function pluginClientRedirectsPages(
options, options,
}; };
const redirects: RedirectMetadata[] = collectRedirects( const redirects: RedirectItem[] = collectRedirects(
pluginContext, pluginContext,
trailingSlash, trailingSlash,
); );
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata( const redirectFiles: RedirectFile[] = toRedirectFiles(
redirects, redirects,
pluginContext, pluginContext,
trailingSlash, trailingSlash,

View file

@ -9,7 +9,9 @@ import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
import type {OptionValidationContext} from '@docusaurus/types'; import type {OptionValidationContext} from '@docusaurus/types';
export type RedirectOption = { export type RedirectOption = {
/** Pathname of an existing Docusaurus page */
to: string; to: string;
/** Pathname of the new page(s) we should create */
from: string | string[]; from: string | string[];
}; };
@ -23,7 +25,9 @@ export type PluginOptions = {
/** The list of redirect rules, each one with multiple `from`s → one `to`. */ /** The list of redirect rules, each one with multiple `from`s → one `to`. */
redirects: RedirectOption[]; redirects: RedirectOption[];
/** /**
* A callback to create a redirect rule. * A callback to create a redirect rule. Docusaurus query this callback
* against every path it has created, and use its return value to output more
* paths.
* @returns All the paths from which we should redirect to `path` * @returns All the paths from which we should redirect to `path`
*/ */
createRedirects?: ( createRedirects?: (

View file

@ -6,14 +6,14 @@
*/ */
import {Joi, PathnameSchema} from '@docusaurus/utils-validation'; import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
import type {RedirectMetadata} from './types'; import type {RedirectItem} from './types';
const RedirectSchema = Joi.object<RedirectMetadata>({ const RedirectSchema = Joi.object<RedirectItem>({
from: PathnameSchema.required(), from: PathnameSchema.required(),
to: PathnameSchema.required(), to: PathnameSchema.required(),
}); });
export function validateRedirect(redirect: RedirectMetadata): void { export function validateRedirect(redirect: RedirectItem): void {
const {error} = RedirectSchema.validate(redirect, { const {error} = RedirectSchema.validate(redirect, {
abortEarly: true, abortEarly: true,
convert: false, convert: false,

View file

@ -21,7 +21,7 @@ export type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
* /!\ easy to be confused: "from" is the new page we should create, * /!\ easy to be confused: "from" is the new page we should create,
* that redirects to "to": the existing Docusaurus page * that redirects to "to": the existing Docusaurus page
*/ */
export type RedirectMetadata = { export type RedirectItem = {
/** Pathname of the new page we should create */ /** Pathname of the new page we should create */
from: string; from: string;
/** Pathname of an existing Docusaurus page */ /** Pathname of an existing Docusaurus page */

View file

@ -13,11 +13,11 @@ import {normalizeUrl} from '@docusaurus/utils';
import createRedirectPageContent from './createRedirectPageContent'; import createRedirectPageContent from './createRedirectPageContent';
import type {PluginContext, RedirectMetadata} from './types'; import type {PluginContext, RedirectItem} from './types';
export type WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>; export type WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>;
export type RedirectFileMetadata = { export type RedirectFile = {
fileAbsolutePath: string; fileAbsolutePath: string;
fileContent: string; fileContent: string;
}; };
@ -57,11 +57,11 @@ function getRedirectFilePath(
return path.join(filePath, `${fileName}/index.html`); return path.join(filePath, `${fileName}/index.html`);
} }
export function toRedirectFilesMetadata( export function toRedirectFiles(
redirects: RedirectMetadata[], redirects: RedirectItem[],
pluginContext: WriteFilesPluginContext, pluginContext: WriteFilesPluginContext,
trailingSlash: boolean | undefined, trailingSlash: boolean | undefined,
): RedirectFileMetadata[] { ): RedirectFile[] {
// Perf: avoid rendering the template twice with the exact same "props" // Perf: avoid rendering the template twice with the exact same "props"
// We might create multiple redirect pages for the same destination url // We might create multiple redirect pages for the same destination url
// note: the first fn arg is the cache key! // note: the first fn arg is the cache key!
@ -69,7 +69,7 @@ export function toRedirectFilesMetadata(
createRedirectPageContent({toUrl}), createRedirectPageContent({toUrl}),
); );
const createFileMetadata = (redirect: RedirectMetadata) => { const createFileMetadata = (redirect: RedirectItem) => {
const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash); const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash);
const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath); const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath);
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to); const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
@ -84,9 +84,7 @@ export function toRedirectFilesMetadata(
return redirects.map(createFileMetadata); return redirects.map(createFileMetadata);
} }
export async function writeRedirectFile( export async function writeRedirectFile(file: RedirectFile): Promise<void> {
file: RedirectFileMetadata,
): Promise<void> {
try { try {
// User-friendly security to prevent file overrides // User-friendly security to prevent file overrides
if (await fs.pathExists(file.fileAbsolutePath)) { if (await fs.pathExists(file.fileAbsolutePath)) {
@ -108,7 +106,7 @@ export async function writeRedirectFile(
} }
export default async function writeRedirectFiles( export default async function writeRedirectFiles(
redirectFiles: RedirectFileMetadata[], redirectFiles: RedirectFile[],
): Promise<void> { ): Promise<void> {
await Promise.all(redirectFiles.map(writeRedirectFile)); await Promise.all(redirectFiles.map(writeRedirectFile));
} }

View file

@ -44,7 +44,7 @@ Accepted fields:
| `fromExtensions` | `string[]` | `[]` | The extensions to be removed from the route after redirecting. | | `fromExtensions` | `string[]` | `[]` | The extensions to be removed from the route after redirecting. |
| `toExtensions` | `string[]` | `[]` | The extensions to be appended to the route after redirecting. | | `toExtensions` | `string[]` | `[]` | The extensions to be appended to the route after redirecting. |
| `redirects` | <code><a href="#RedirectRule">RedirectRule</a>[]</code> | `[]` | The list of redirect rules. | | `redirects` | <code><a href="#RedirectRule">RedirectRule</a>[]</code> | `[]` | The list of redirect rules. |
| `createRedirects` | <code><a href="#CreateRedirectsFn">CreateRedirectsFn</a></code> | `undefined` | A callback to create a redirect rule. | | `createRedirects` | <code><a href="#CreateRedirectsFn">CreateRedirectsFn</a></code> | `undefined` | A callback to create a redirect rule. Docusaurus query this callback against every path it has created, and use its return value to output more paths. |
```mdx-code-block ```mdx-code-block
</APITable> </APITable>
@ -61,9 +61,20 @@ type RedirectRule = {
}; };
``` ```
:::note
The idea of "from" and "to" is central in this plugin. "From" means a path that you want to _create_, i.e. an extra HTML file that will be written; "to" means a path to want to redirect _to_, usually a route that Docusaurus already knows about.
This is why you can have multiple "from" for the same "to": we will create multiple HTML files that all redirect to the same destination. On the other hand, one "from" can never have more than one "to": the written HTML file needs to have a determinate destination.
:::
#### `CreateRedirectsFn` {#CreateRedirectsFn} #### `CreateRedirectsFn` {#CreateRedirectsFn}
```ts ```ts
// The parameter `path` is a route that Docusaurus has already created. It can
// be seen as the "to", and your return value is the "from". Returning a falsy
// value will not create any redirect pages for this particular path.
type CreateRedirectsFn = (path: string) => string[] | string | null | undefined; type CreateRedirectsFn = (path: string) => string[] | string | null | undefined;
``` ```