mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-22 21:47:01 +02:00
Add redirects plugin option
This commit is contained in:
parent
a0991c581b
commit
1439bad84c
8 changed files with 218 additions and 21 deletions
|
@ -0,0 +1,21 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`collectRedirects should throw if plugin option redirects contain invalid to paths 1`] = `
|
||||
"You are trying to create client-side redirections to paths that do not exist:
|
||||
- /this/path/does/not/exist2
|
||||
- /this/path/does/not/exist2
|
||||
|
||||
Valid paths you can redirect to:
|
||||
- /
|
||||
- /someExistingPath
|
||||
- /anotherExistingPath
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`collectRedirects should throw if redirect creator creates invalid redirects 1`] = `
|
||||
"Some created redirects are invalid:
|
||||
- {\\"fromRoutePath\\":\\"https://google.com/\\",\\"toRoutePath\\":\\"/\\"} => Validation error: fromRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
- {\\"fromRoutePath\\":\\"//abc\\",\\"toRoutePath\\":\\"/\\"} => Validation error: fromRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
- {\\"fromRoutePath\\":\\"/def?queryString=toto\\",\\"toRoutePath\\":\\"/\\"} => Validation error: fromRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
"
|
||||
`;
|
|
@ -1,16 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 1`] = `
|
||||
"fromRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
Redirect={\\"fromRoutePath\\":\\"https://fb.com/fromSomePath\\",\\"toRoutePath\\":\\"/toSomePath\\"}"
|
||||
`;
|
||||
exports[`validateRedirect throw for bad redirects 1`] = `"{\\"fromRoutePath\\":\\"https://fb.com/fromSomePath\\",\\"toRoutePath\\":\\"/toSomePath\\"} => Validation error: fromRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 2`] = `
|
||||
"toRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
Redirect={\\"fromRoutePath\\":\\"/fromSomePath\\",\\"toRoutePath\\":\\"https://fb.com/toSomePath\\"}"
|
||||
`;
|
||||
exports[`validateRedirect throw for bad redirects 2`] = `"{\\"fromRoutePath\\":\\"/fromSomePath\\",\\"toRoutePath\\":\\"https://fb.com/toSomePath\\"} => Validation error: toRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 3`] = `
|
||||
"toRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
Redirect={\\"fromRoutePath\\":\\"/fromSomePath\\",\\"toRoutePath\\":\\"/toSomePath?queryString=xyz\\"}"
|
||||
`;
|
||||
exports[`validateRedirect throw for bad redirects 3`] = `"{\\"fromRoutePath\\":\\"/fromSomePath\\",\\"toRoutePath\\":\\"/toSomePath?queryString=xyz\\"} => Validation error: toRoutePath is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
|
|
@ -73,6 +73,67 @@ describe('collectRedirects', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should collect redirects from plugin option redirects', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
redirects: [
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/somePath',
|
||||
},
|
||||
{
|
||||
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
|
||||
to: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
['/', '/somePath'],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
fromRoutePath: '/someLegacyPath',
|
||||
toRoutePath: '/somePath',
|
||||
},
|
||||
{
|
||||
fromRoutePath: '/someLegacyPathArray1',
|
||||
toRoutePath: '/',
|
||||
},
|
||||
{
|
||||
fromRoutePath: '/someLegacyPathArray2',
|
||||
toRoutePath: '/',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should throw if plugin option redirects contain invalid to paths', () => {
|
||||
expect(() =>
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
redirects: [
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/this/path/does/not/exist2',
|
||||
},
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/this/path/does/not/exist2',
|
||||
},
|
||||
],
|
||||
},
|
||||
['/', '/someExistingPath', '/anotherExistingPath'],
|
||||
),
|
||||
),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should collect redirects with custom redirect creator', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
|
@ -118,6 +179,28 @@ describe('collectRedirects', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should throw if redirect creator creates invalid redirects', () => {
|
||||
expect(() =>
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
createRedirects: (routePath) => {
|
||||
if (routePath === '/') {
|
||||
return [
|
||||
`https://google.com/`,
|
||||
`//abc`,
|
||||
`/def?queryString=toto`,
|
||||
];
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
['/'],
|
||||
),
|
||||
),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should filter unwanted redirects', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
|
|
|
@ -36,11 +36,13 @@ describe('normalizePluginOptions', () => {
|
|||
fromExtensions: ['exe', 'zip'],
|
||||
toExtensions: ['html'],
|
||||
createRedirects,
|
||||
redirects: [{from: '/x', to: '/y'}],
|
||||
}),
|
||||
).toEqual({
|
||||
fromExtensions: ['exe', 'zip'],
|
||||
toExtensions: ['html'],
|
||||
createRedirects,
|
||||
redirects: [{from: '/x', to: '/y'}],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,25 +5,67 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {flatten, uniqBy} from 'lodash';
|
||||
import {flatten, uniqBy, difference} from 'lodash';
|
||||
import {
|
||||
RedirectsCreator,
|
||||
PluginContext,
|
||||
RedirectMetadata,
|
||||
PluginOptions,
|
||||
RedirectOption,
|
||||
} from './types';
|
||||
import {
|
||||
fromExtensionsRedirectCreator,
|
||||
toExtensionsRedirectCreator,
|
||||
} from './redirectCreators';
|
||||
import {validateRedirect} from './redirectValidation';
|
||||
|
||||
export default function collectRedirects(
|
||||
pluginContext: PluginContext,
|
||||
): RedirectMetadata[] {
|
||||
const redirects = doCollectRedirects(pluginContext);
|
||||
validateCollectedRedirects(redirects, pluginContext);
|
||||
return filterUnwantedRedirects(redirects, pluginContext);
|
||||
}
|
||||
|
||||
function validateCollectedRedirects(
|
||||
redirects: RedirectMetadata[],
|
||||
pluginContext: PluginContext,
|
||||
) {
|
||||
const redirectValidationErrors: string[] = redirects
|
||||
.map((redirect) => {
|
||||
try {
|
||||
validateRedirect(redirect);
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (redirectValidationErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Some created redirects are invalid:
|
||||
- ${redirectValidationErrors.join('\n- ')}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowedToPaths = pluginContext.routesPaths;
|
||||
|
||||
const toPaths = redirects.map((redirect) => redirect.toRoutePath);
|
||||
|
||||
const illegalToPaths = difference(toPaths, allowedToPaths);
|
||||
|
||||
if (illegalToPaths.length > 0) {
|
||||
throw new Error(
|
||||
`You are trying to create client-side redirections to paths that do not exist:
|
||||
- ${illegalToPaths.join('\n- ')}
|
||||
|
||||
Valid paths you can redirect to:
|
||||
- ${allowedToPaths.join('\n- ')}
|
||||
`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnwantedRedirects(
|
||||
redirects: RedirectMetadata[],
|
||||
pluginContext: PluginContext,
|
||||
|
@ -46,20 +88,42 @@ function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] {
|
|||
pluginContext.options,
|
||||
);
|
||||
|
||||
return flatten(
|
||||
const optionsRedirects = collectPluginOptionRedirects(pluginContext);
|
||||
|
||||
const redirectCreatorsRedirects = flatten(
|
||||
redirectsCreators.map((redirectCreator) => {
|
||||
return createRoutesPathsRedirects(redirectCreator, pluginContext);
|
||||
}),
|
||||
);
|
||||
|
||||
return [...optionsRedirects, ...redirectCreatorsRedirects];
|
||||
}
|
||||
|
||||
function collectPluginOptionRedirects(
|
||||
pluginContext: PluginContext,
|
||||
): RedirectMetadata[] {
|
||||
// For conveniency, user can use a string or a string[]
|
||||
function optionToRedirects(option: RedirectOption): RedirectMetadata[] {
|
||||
if (typeof option.from === 'string') {
|
||||
return [{fromRoutePath: option.from, toRoutePath: option.to}];
|
||||
} else {
|
||||
return option.from.map((fromRoutePath) => ({
|
||||
fromRoutePath,
|
||||
toRoutePath: option.to,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return flatten(pluginContext.options.redirects.map(optionToRedirects));
|
||||
}
|
||||
|
||||
function buildRedirectCreators(options: PluginOptions): RedirectsCreator[] {
|
||||
const noopRedirectCreator: RedirectsCreator = (_routePath: string) => [];
|
||||
return [
|
||||
const redirectCreators = [
|
||||
fromExtensionsRedirectCreator(options.fromExtensions),
|
||||
toExtensionsRedirectCreator(options.toExtensions),
|
||||
options.createRedirects ?? noopRedirectCreator,
|
||||
];
|
||||
options.createRedirects && redirectCreators.push(options.createRedirects);
|
||||
return redirectCreators;
|
||||
}
|
||||
|
||||
// Create all redirects for a list of route path
|
||||
|
|
|
@ -5,12 +5,19 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {PluginOptions, RedirectsCreator, UserPluginOptions} from './types';
|
||||
import {
|
||||
PluginOptions,
|
||||
RedirectOption,
|
||||
RedirectsCreator,
|
||||
UserPluginOptions,
|
||||
} from './types';
|
||||
import * as Yup from 'yup';
|
||||
import {PathnameValidator} from './redirectValidation';
|
||||
|
||||
export const DefaultPluginOptions: PluginOptions = {
|
||||
fromExtensions: [],
|
||||
toExtensions: [],
|
||||
redirects: [],
|
||||
};
|
||||
|
||||
function isRedirectsCreator(value: any): value is RedirectsCreator | undefined {
|
||||
|
@ -20,9 +27,28 @@ function isRedirectsCreator(value: any): value is RedirectsCreator | undefined {
|
|||
return value instanceof Function;
|
||||
}
|
||||
|
||||
const RedirectPluginOptionValidation = Yup.object<RedirectOption>({
|
||||
to: PathnameValidator.required(),
|
||||
// wasn't able to use .when("from")...had cyclic dependency error
|
||||
// (https://stackoverflow.com/a/56866941/82609)
|
||||
from: Yup.mixed<string | string[]>().test({
|
||||
name: 'from',
|
||||
message: '${path} contains invalid redirection value',
|
||||
test: (from) => {
|
||||
return Array.isArray(from)
|
||||
? Yup.array()
|
||||
.of(PathnameValidator.required())
|
||||
.required()
|
||||
.isValidSync(from)
|
||||
: PathnameValidator.required().isValidSync(from);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const UserOptionsSchema = Yup.object().shape<UserPluginOptions>({
|
||||
fromExtensions: Yup.array().of(Yup.string().required().min(0)),
|
||||
toExtensions: Yup.array().of(Yup.string().required().min(0)),
|
||||
redirects: Yup.array().of(RedirectPluginOptionValidation) as any, // TODO Yup expect weird typing here
|
||||
createRedirects: Yup.mixed().test(
|
||||
'createRedirects',
|
||||
'createRedirects should be a function',
|
||||
|
|
|
@ -16,9 +16,11 @@ const validPathnameTest: Yup.TestOptions = {
|
|||
test: isValidPathname,
|
||||
};
|
||||
|
||||
export const PathnameValidator = Yup.string().test(validPathnameTest);
|
||||
|
||||
const RedirectSchema = Yup.object<RedirectMetadata>({
|
||||
fromRoutePath: Yup.string().required().test(validPathnameTest),
|
||||
toRoutePath: Yup.string().required().test(validPathnameTest),
|
||||
fromRoutePath: PathnameValidator.required(),
|
||||
toRoutePath: PathnameValidator.required(),
|
||||
});
|
||||
|
||||
export function validateRedirect(redirect: RedirectMetadata) {
|
||||
|
@ -26,6 +28,8 @@ export function validateRedirect(redirect: RedirectMetadata) {
|
|||
RedirectSchema.validateSync(redirect);
|
||||
} catch (e) {
|
||||
// Tells the user which redirect is the problem!
|
||||
throw new Error(`${e.message}\nRedirect=${JSON.stringify(redirect)}`);
|
||||
throw new Error(
|
||||
`${JSON.stringify(redirect)} => Validation error: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,15 @@ import {Props} from '@docusaurus/types';
|
|||
export type PluginOptions = {
|
||||
fromExtensions: string[];
|
||||
toExtensions: string[];
|
||||
redirects: RedirectOption[];
|
||||
createRedirects?: RedirectsCreator;
|
||||
};
|
||||
|
||||
export type RedirectOption = {
|
||||
to: string;
|
||||
from: string | string[];
|
||||
};
|
||||
|
||||
export type UserPluginOptions = Partial<PluginOptions>;
|
||||
|
||||
// The minimal infos the plugin needs to work
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue