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
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>
<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>
<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>
<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>
<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>
<html>

View file

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

View file

@ -7,52 +7,57 @@
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
applyTrailingSlash,
type ApplyTrailingSlashParams,
} from '@docusaurus/utils-common';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import {
createFromExtensionsRedirects,
createToExtensionsRedirects,
} from './extensionRedirects';
import {validateRedirect} from './redirectValidation';
import type {PluginOptions, RedirectOption} from './options';
import type {PluginContext, RedirectMetadata} from './types';
import type {PluginContext, RedirectItem} from './types';
export default function collectRedirects(
pluginContext: PluginContext,
trailingSlash: boolean | undefined,
): RedirectMetadata[] {
let redirects = doCollectRedirects(pluginContext);
redirects = applyRedirectsTrailingSlash(redirects, {
trailingSlash,
baseUrl: pluginContext.baseUrl,
});
): RedirectItem[] {
// For each plugin config option, create the appropriate redirects
const 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,
baseUrl: pluginContext.baseUrl,
}),
}));
validateCollectedRedirects(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(
redirects: RedirectMetadata[],
redirects: RedirectItem[],
pluginContext: PluginContext,
) {
const redirectValidationErrors = redirects
@ -89,9 +94,9 @@ Valid paths you can redirect to:
}
function filterUnwantedRedirects(
redirects: RedirectMetadata[],
redirects: RedirectItem[],
pluginContext: PluginContext,
): RedirectMetadata[] {
): RedirectItem[] {
// We don't want to create the same redirect twice, since that would lead to
// writing the same html redirection file twice.
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(
redirectsOption: PluginOptions['redirects'],
): RedirectMetadata[] {
): RedirectItem[] {
// 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') {
return [{from: option.from, to: option.to}];
}
return option.from.map((from) => ({
from,
to: option.to,
}));
return option.from.map((from) => ({from, to: option.to}));
}
return redirectsOption.flatMap(optionToRedirects);
@ -160,17 +143,14 @@ function createRedirectsOptionRedirects(
function createCreateRedirectsOptionRedirects(
paths: string[],
createRedirects: PluginOptions['createRedirects'],
): RedirectMetadata[] {
function createPathRedirects(path: string): RedirectMetadata[] {
): RedirectItem[] {
function createPathRedirects(path: string): RedirectItem[] {
const fromsMixed: string | string[] = createRedirects?.(path) ?? [];
const froms: string[] =
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
return froms.map((from) => ({
from,
to: path,
}));
return froms.map((from) => ({from, to: path}));
}
return paths.flatMap(createPathRedirects);

View file

@ -10,7 +10,7 @@ import {
removeSuffix,
removeTrailingSlash,
} from '@docusaurus/utils';
import type {RedirectMetadata} from './types';
import type {RedirectItem} from './types';
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.';
@ -40,23 +40,21 @@ const validateExtension = (ext: string) => {
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(
paths: string[],
extensions: string[],
): RedirectMetadata[] {
): RedirectItem[] {
extensions.forEach(validateExtension);
const dottedExtensions = extensions.map(addLeadingDot);
const createPathRedirects = (path: string): RedirectMetadata[] => {
const createPathRedirects = (path: string): RedirectItem[] => {
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
if (extensionFound) {
const routePathWithoutExtension = removeSuffix(path, extensionFound);
return [routePathWithoutExtension].map((from) => ({
from,
to: path,
}));
return [{from: removeSuffix(path, extensionFound), to: path}];
}
return [];
};
@ -64,12 +62,15 @@ export function createToExtensionsRedirects(
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(
paths: string[],
extensions: string[],
): RedirectMetadata[] {
): RedirectItem[] {
extensions.forEach(validateExtension);
const dottedExtensions = extensions.map(addLeadingDot);
@ -77,7 +78,7 @@ export function createFromExtensionsRedirects(
const alreadyEndsWithAnExtension = (str: string) =>
dottedExtensions.some((ext) => str.endsWith(ext));
const createPathRedirects = (path: string): RedirectMetadata[] => {
const createPathRedirects = (path: string): RedirectItem[] => {
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
return [];
}

View file

@ -8,11 +8,11 @@
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
import collectRedirects from './collectRedirects';
import writeRedirectFiles, {
toRedirectFilesMetadata,
type RedirectFileMetadata,
toRedirectFiles,
type RedirectFile,
} from './writeRedirectFiles';
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';
export default function pluginClientRedirectsPages(
@ -33,12 +33,12 @@ export default function pluginClientRedirectsPages(
options,
};
const redirects: RedirectMetadata[] = collectRedirects(
const redirects: RedirectItem[] = collectRedirects(
pluginContext,
trailingSlash,
);
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
const redirectFiles: RedirectFile[] = toRedirectFiles(
redirects,
pluginContext,
trailingSlash,

View file

@ -9,7 +9,9 @@ import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
import type {OptionValidationContext} from '@docusaurus/types';
export type RedirectOption = {
/** Pathname of an existing Docusaurus page */
to: string;
/** Pathname of the new page(s) we should create */
from: string | string[];
};
@ -23,7 +25,9 @@ export type PluginOptions = {
/** The list of redirect rules, each one with multiple `from`s → one `to`. */
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`
*/
createRedirects?: (

View file

@ -6,14 +6,14 @@
*/
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(),
to: PathnameSchema.required(),
});
export function validateRedirect(redirect: RedirectMetadata): void {
export function validateRedirect(redirect: RedirectItem): void {
const {error} = RedirectSchema.validate(redirect, {
abortEarly: true,
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,
* that redirects to "to": the existing Docusaurus page
*/
export type RedirectMetadata = {
export type RedirectItem = {
/** Pathname of the new page we should create */
from: string;
/** Pathname of an existing Docusaurus page */

View file

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