Add redirects plugin option

This commit is contained in:
slorber 2020-06-03 17:30:21 +02:00
parent a0991c581b
commit 1439bad84c
8 changed files with 218 additions and 21 deletions

View file

@ -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
"
`;

View file

@ -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"`;

View file

@ -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(

View file

@ -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'}],
});
});

View file

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

View file

@ -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',

View file

@ -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}`,
);
}
}

View file

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