Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Expand Up @@ -9,7 +9,7 @@ import fs from 'fs-extra';
import path from 'path';

import writeRedirectFiles, {
toRedirectFilesMetadata,
toRedirectFiles,
createToUrl,
} from '../writeRedirectFiles';

Expand Down Expand Up @@ -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'},
Expand All @@ -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'},
Expand All @@ -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'},
Expand Down Expand Up @@ -132,7 +132,7 @@ describe('toRedirectFilesMetadata', () => {
outDir: '/tmp/someFixedOutDir',
baseUrl: '/',
};
const redirectFiles = toRedirectFilesMetadata(
const redirectFiles = toRedirectFiles(
[{from: '/abc.html', to: '/abc'}],
pluginContext,
undefined,
Expand All @@ -147,7 +147,7 @@ describe('toRedirectFilesMetadata', () => {
outDir: '/tmp/someFixedOutDir',
baseUrl: '',
};
const redirectFiles = toRedirectFilesMetadata(
const redirectFiles = toRedirectFiles(
[{from: '/abc.html', to: '/abc'}],
pluginContext,
undefined,
Expand Down
106 changes: 43 additions & 63 deletions packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Expand Up @@ -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.';
Expand Down Expand Up @@ -40,44 +40,45 @@ 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 [];
};

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);

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 [];
}
Expand Down
10 changes: 5 additions & 5 deletions packages/docusaurus-plugin-client-redirects/src/index.ts
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
Expand Up @@ -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[];
};

Expand All @@ -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?: (
Expand Down