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 // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateRedirect throw for bad redirects 1`] = ` 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"`;
"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 2`] = ` 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"`;
"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 3`] = ` 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"`;
"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\\"}"
`;

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', () => { test('should collect redirects with custom redirect creator', () => {
expect( expect(
collectRedirects( 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', () => { test('should filter unwanted redirects', () => {
expect( expect(
collectRedirects( collectRedirects(

View file

@ -36,11 +36,13 @@ describe('normalizePluginOptions', () => {
fromExtensions: ['exe', 'zip'], fromExtensions: ['exe', 'zip'],
toExtensions: ['html'], toExtensions: ['html'],
createRedirects, createRedirects,
redirects: [{from: '/x', to: '/y'}],
}), }),
).toEqual({ ).toEqual({
fromExtensions: ['exe', 'zip'], fromExtensions: ['exe', 'zip'],
toExtensions: ['html'], toExtensions: ['html'],
createRedirects, createRedirects,
redirects: [{from: '/x', to: '/y'}],
}); });
}); });

View file

@ -5,25 +5,67 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {flatten, uniqBy} from 'lodash'; import {flatten, uniqBy, difference} from 'lodash';
import { import {
RedirectsCreator, RedirectsCreator,
PluginContext, PluginContext,
RedirectMetadata, RedirectMetadata,
PluginOptions, PluginOptions,
RedirectOption,
} from './types'; } from './types';
import { import {
fromExtensionsRedirectCreator, fromExtensionsRedirectCreator,
toExtensionsRedirectCreator, toExtensionsRedirectCreator,
} from './redirectCreators'; } from './redirectCreators';
import {validateRedirect} from './redirectValidation';
export default function collectRedirects( export default function collectRedirects(
pluginContext: PluginContext, pluginContext: PluginContext,
): RedirectMetadata[] { ): RedirectMetadata[] {
const redirects = doCollectRedirects(pluginContext); const redirects = doCollectRedirects(pluginContext);
validateCollectedRedirects(redirects, pluginContext);
return filterUnwantedRedirects(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( function filterUnwantedRedirects(
redirects: RedirectMetadata[], redirects: RedirectMetadata[],
pluginContext: PluginContext, pluginContext: PluginContext,
@ -46,20 +88,42 @@ function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] {
pluginContext.options, pluginContext.options,
); );
return flatten( const optionsRedirects = collectPluginOptionRedirects(pluginContext);
const redirectCreatorsRedirects = flatten(
redirectsCreators.map((redirectCreator) => { redirectsCreators.map((redirectCreator) => {
return createRoutesPathsRedirects(redirectCreator, pluginContext); 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[] { function buildRedirectCreators(options: PluginOptions): RedirectsCreator[] {
const noopRedirectCreator: RedirectsCreator = (_routePath: string) => []; const redirectCreators = [
return [
fromExtensionsRedirectCreator(options.fromExtensions), fromExtensionsRedirectCreator(options.fromExtensions),
toExtensionsRedirectCreator(options.toExtensions), toExtensionsRedirectCreator(options.toExtensions),
options.createRedirects ?? noopRedirectCreator,
]; ];
options.createRedirects && redirectCreators.push(options.createRedirects);
return redirectCreators;
} }
// Create all redirects for a list of route path // 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. * 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 * as Yup from 'yup';
import {PathnameValidator} from './redirectValidation';
export const DefaultPluginOptions: PluginOptions = { export const DefaultPluginOptions: PluginOptions = {
fromExtensions: [], fromExtensions: [],
toExtensions: [], toExtensions: [],
redirects: [],
}; };
function isRedirectsCreator(value: any): value is RedirectsCreator | undefined { function isRedirectsCreator(value: any): value is RedirectsCreator | undefined {
@ -20,9 +27,28 @@ function isRedirectsCreator(value: any): value is RedirectsCreator | undefined {
return value instanceof Function; 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>({ const UserOptionsSchema = Yup.object().shape<UserPluginOptions>({
fromExtensions: Yup.array().of(Yup.string().required().min(0)), fromExtensions: Yup.array().of(Yup.string().required().min(0)),
toExtensions: 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: Yup.mixed().test(
'createRedirects', 'createRedirects',
'createRedirects should be a function', 'createRedirects should be a function',

View file

@ -16,9 +16,11 @@ const validPathnameTest: Yup.TestOptions = {
test: isValidPathname, test: isValidPathname,
}; };
export const PathnameValidator = Yup.string().test(validPathnameTest);
const RedirectSchema = Yup.object<RedirectMetadata>({ const RedirectSchema = Yup.object<RedirectMetadata>({
fromRoutePath: Yup.string().required().test(validPathnameTest), fromRoutePath: PathnameValidator.required(),
toRoutePath: Yup.string().required().test(validPathnameTest), toRoutePath: PathnameValidator.required(),
}); });
export function validateRedirect(redirect: RedirectMetadata) { export function validateRedirect(redirect: RedirectMetadata) {
@ -26,6 +28,8 @@ export function validateRedirect(redirect: RedirectMetadata) {
RedirectSchema.validateSync(redirect); RedirectSchema.validateSync(redirect);
} catch (e) { } catch (e) {
// Tells the user which redirect is the problem! // 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 = { export type PluginOptions = {
fromExtensions: string[]; fromExtensions: string[];
toExtensions: string[]; toExtensions: string[];
redirects: RedirectOption[];
createRedirects?: RedirectsCreator; createRedirects?: RedirectsCreator;
}; };
export type RedirectOption = {
to: string;
from: string | string[];
};
export type UserPluginOptions = Partial<PluginOptions>; export type UserPluginOptions = Partial<PluginOptions>;
// The minimal infos the plugin needs to work // The minimal infos the plugin needs to work