mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 07:37:19 +02:00
Merge pull request #2793 from slorber/feature/client-side-redirects
feat(v2): docusaurus-plugin-client-redirects
This commit is contained in:
commit
68a1bb1ebf
32 changed files with 1762 additions and 0 deletions
|
@ -14,6 +14,7 @@ packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js
|
|||
packages/docusaurus-utils/lib/
|
||||
packages/docusaurus/lib/
|
||||
packages/docusaurus-init/lib/
|
||||
packages/docusaurus-plugin-client-redirects/lib/
|
||||
packages/docusaurus-plugin-content-blog/lib/
|
||||
packages/docusaurus-plugin-content-docs/lib/
|
||||
packages/docusaurus-plugin-content-pages/lib/
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,6 +18,7 @@ types
|
|||
packages/docusaurus-utils/lib/
|
||||
packages/docusaurus/lib/
|
||||
packages/docusaurus-init/lib/
|
||||
packages/docusaurus-plugin-client-redirects/lib/
|
||||
packages/docusaurus-plugin-content-blog/lib/
|
||||
packages/docusaurus-plugin-content-docs/lib/
|
||||
packages/docusaurus-plugin-content-pages/lib/
|
||||
|
|
|
@ -6,6 +6,7 @@ coverage
|
|||
packages/docusaurus-utils/lib/
|
||||
packages/docusaurus/lib/
|
||||
packages/docusaurus-init/lib/
|
||||
packages/docusaurus-plugin-client-redirects/lib/
|
||||
packages/docusaurus-init/templates/**/*.md
|
||||
packages/docusaurus-plugin-content-blog/lib/
|
||||
packages/docusaurus-plugin-content-docs/lib/
|
||||
|
|
31
packages/docusaurus-plugin-client-redirects/package.json
Normal file
31
packages/docusaurus-plugin-client-redirects/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@docusaurus/plugin-client-redirects",
|
||||
"version": "2.0.0-alpha.56",
|
||||
"description": "Client redirects plugin for Docusaurus",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/types": "^2.0.0-alpha.56",
|
||||
"@docusaurus/utils": "^2.0.0-alpha.56",
|
||||
"eta": "^1.1.1",
|
||||
"globby": "^10.0.1",
|
||||
"yup": "^0.29.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@docusaurus/core": "^2.0.0",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yup": "^0.29.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// 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 array of array redirect 1`] = `
|
||||
"Some created redirects are invalid:
|
||||
- {\\"from\\":[\\"/fromPath\\"],\\"to\\":\\"/\\"} => Validation error: from must be a \`string\` type, but the final value was: \`[
|
||||
\\"\\\\\\"/fromPath\\\\\\"\\"
|
||||
]\`.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`collectRedirects should throw if redirect creator creates invalid redirects 1`] = `
|
||||
"Some created redirects are invalid:
|
||||
- {\\"from\\":\\"https://google.com/\\",\\"to\\":\\"/\\"} => Validation error: from is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
- {\\"from\\":\\"//abc\\",\\"to\\":\\"/\\"} => Validation error: from is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
- {\\"from\\":\\"/def?queryString=toto\\",\\"to\\":\\"/\\"} => Validation error: from is not a valid pathname. Pathname should start with / and not contain any domain or query string
|
||||
"
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`createRedirectPageContent should encode uri special chars 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/gr/%CF%83%CE%B5%CE%BB%CE%B9%CE%B4%CE%B1%CF%82/\\">
|
||||
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/gr/%CF%83%CE%B5%CE%BB%CE%B9%CE%B4%CE%B1%CF%82/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = 'https://docusaurus.io/gr/%CF%83%CE%B5%CE%BB%CE%B9%CE%B4%CE%B1%CF%82/';
|
||||
</script>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`createRedirectPageContent should match snapshot 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
|
||||
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = 'https://docusaurus.io/';
|
||||
</script>
|
||||
</html>"
|
||||
`;
|
|
@ -0,0 +1,37 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`normalizePluginOptions should reject bad createRedirects user inputs 1`] = `
|
||||
"Invalid @docusaurus/plugin-client-redirects options: createRedirects should be a function
|
||||
{
|
||||
\\"createRedirects\\": [
|
||||
\\"bad\\",
|
||||
\\"value\\"
|
||||
]
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`normalizePluginOptions should reject bad fromExtensions user inputs 1`] = `
|
||||
"Invalid @docusaurus/plugin-client-redirects options: fromExtensions[0] must be a \`string\` type, but the final value was: \`null\`.
|
||||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`
|
||||
{
|
||||
\\"fromExtensions\\": [
|
||||
null,
|
||||
null,
|
||||
123,
|
||||
true
|
||||
]
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`normalizePluginOptions should reject bad toExtensions user inputs 1`] = `
|
||||
"Invalid @docusaurus/plugin-client-redirects options: toExtensions[0] must be a \`string\` type, but the final value was: \`null\`.
|
||||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`
|
||||
{
|
||||
\\"toExtensions\\": [
|
||||
null,
|
||||
null,
|
||||
123,
|
||||
true
|
||||
]
|
||||
}"
|
||||
`;
|
|
@ -0,0 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 1`] = `"{\\"from\\":\\"https://fb.com/fromSomePath\\",\\"to\\":\\"/toSomePath\\"} => Validation error: from is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 2`] = `"{\\"from\\":\\"/fromSomePath\\",\\"to\\":\\"https://fb.com/toSomePath\\"} => Validation error: to is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 3`] = `"{\\"from\\":\\"/fromSomePath\\",\\"to\\":\\"/toSomePath?queryString=xyz\\"} => Validation error: to is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 4`] = `"{\\"from\\":null,\\"to\\":\\"/toSomePath?queryString=xyz\\"} => Validation error: to is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
||||
|
||||
exports[`validateRedirect throw for bad redirects 5`] = `"{\\"from\\":[\\"heyho\\"],\\"to\\":\\"/toSomePath?queryString=xyz\\"} => Validation error: to is not a valid pathname. Pathname should start with / and not contain any domain or query string"`;
|
|
@ -0,0 +1,71 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`toRedirectFilesMetadata should create appropriate metadatas for empty baseUrl: fileContent baseUrl=empty 1`] = `
|
||||
Array [
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=/abc/\\">
|
||||
<link rel=\\"canonical\\" href=\\"/abc/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = '/abc/';
|
||||
</script>
|
||||
</html>",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`toRedirectFilesMetadata should create appropriate metadatas for root baseUrl: fileContent baseUrl=/ 1`] = `
|
||||
Array [
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=/abc/\\">
|
||||
<link rel=\\"canonical\\" href=\\"/abc/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = '/abc/';
|
||||
</script>
|
||||
</html>",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`toRedirectFilesMetadata should create appropriate metadatas: fileContent 1`] = `
|
||||
Array [
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/abc/\\">
|
||||
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/abc/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = 'https://docusaurus.io/abc/';
|
||||
</script>
|
||||
</html>",
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/def.html/\\">
|
||||
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/def.html/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = 'https://docusaurus.io/def.html/';
|
||||
</script>
|
||||
</html>",
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
|
||||
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = 'https://docusaurus.io/';
|
||||
</script>
|
||||
</html>",
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,255 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {PluginContext, UserPluginOptions} from '../types';
|
||||
import collectRedirects from '../collectRedirects';
|
||||
import normalizePluginOptions from '../normalizePluginOptions';
|
||||
import {removeTrailingSlash} from '@docusaurus/utils';
|
||||
|
||||
function createTestPluginContext(
|
||||
options?: UserPluginOptions,
|
||||
routesPaths: string[] = [],
|
||||
): PluginContext {
|
||||
return {
|
||||
outDir: '/tmp',
|
||||
baseUrl: 'https://docusaurus.io',
|
||||
routesPaths: routesPaths,
|
||||
options: normalizePluginOptions(options),
|
||||
};
|
||||
}
|
||||
|
||||
describe('collectRedirects', () => {
|
||||
test('should collect no redirect for undefined config', () => {
|
||||
expect(
|
||||
collectRedirects(createTestPluginContext(undefined, ['/', '/path'])),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('should collect no redirect for empty config', () => {
|
||||
expect(collectRedirects(createTestPluginContext({}))).toEqual([]);
|
||||
});
|
||||
|
||||
test('should collect redirects to html/exe extension', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
fromExtensions: ['html', 'exe'],
|
||||
},
|
||||
['/', '/somePath', '/otherPath.html'],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from: '/somePath.html',
|
||||
to: '/somePath',
|
||||
},
|
||||
{
|
||||
from: '/somePath.exe',
|
||||
to: '/somePath',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should collect redirects to html/exe extension', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
toExtensions: ['html', 'exe'],
|
||||
},
|
||||
['/', '/somePath', '/otherPath.html'],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from: '/otherPath',
|
||||
to: '/otherPath.html',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should collect redirects from plugin option redirects', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
redirects: [
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/somePath',
|
||||
},
|
||||
{
|
||||
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
|
||||
to: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
['/', '/somePath'],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from: '/someLegacyPath',
|
||||
to: '/somePath',
|
||||
},
|
||||
{
|
||||
from: '/someLegacyPathArray1',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
from: '/someLegacyPathArray2',
|
||||
to: '/',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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(
|
||||
createTestPluginContext(
|
||||
{
|
||||
createRedirects: (routePath) => {
|
||||
return [
|
||||
`${removeTrailingSlash(routePath)}/some/path/suffix1`,
|
||||
`${removeTrailingSlash(routePath)}/some/other/path/suffix2`,
|
||||
];
|
||||
},
|
||||
},
|
||||
['/', '/testpath', '/otherPath.html'],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from: '/some/path/suffix1',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
from: '/some/other/path/suffix2',
|
||||
to: '/',
|
||||
},
|
||||
|
||||
{
|
||||
from: '/testpath/some/path/suffix1',
|
||||
to: '/testpath',
|
||||
},
|
||||
{
|
||||
from: '/testpath/some/other/path/suffix2',
|
||||
to: '/testpath',
|
||||
},
|
||||
|
||||
{
|
||||
from: '/otherPath.html/some/path/suffix1',
|
||||
to: '/otherPath.html',
|
||||
},
|
||||
{
|
||||
from: '/otherPath.html/some/other/path/suffix2',
|
||||
to: '/otherPath.html',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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 throw if redirect creator creates array of array redirect', () => {
|
||||
expect(() =>
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
createRedirects: (routePath) => {
|
||||
if (routePath === '/') {
|
||||
return [[`/fromPath`]] as any;
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
['/'],
|
||||
),
|
||||
),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should filter unwanted redirects', () => {
|
||||
expect(
|
||||
collectRedirects(
|
||||
createTestPluginContext(
|
||||
{
|
||||
fromExtensions: ['html', 'exe'],
|
||||
toExtensions: ['html', 'exe'],
|
||||
},
|
||||
[
|
||||
'/',
|
||||
'/somePath',
|
||||
'/somePath.html',
|
||||
'/somePath.exe',
|
||||
'/fromShouldWork.html',
|
||||
'/toShouldWork',
|
||||
],
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
from: '/toShouldWork.html',
|
||||
to: '/toShouldWork',
|
||||
},
|
||||
{
|
||||
from: '/toShouldWork.exe',
|
||||
to: '/toShouldWork',
|
||||
},
|
||||
{
|
||||
from: '/fromShouldWork',
|
||||
to: '/fromShouldWork.html',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import createRedirectPageContent from '../createRedirectPageContent';
|
||||
|
||||
describe('createRedirectPageContent', () => {
|
||||
test('should match snapshot', () => {
|
||||
expect(
|
||||
createRedirectPageContent({toUrl: 'https://docusaurus.io/'}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should encode uri special chars', () => {
|
||||
const result = createRedirectPageContent({
|
||||
toUrl: 'https://docusaurus.io/gr/σελιδας/',
|
||||
});
|
||||
expect(result).toContain(
|
||||
'https://docusaurus.io/gr/%CF%83%CE%B5%CE%BB%CE%B9%CE%B4%CE%B1%CF%82/',
|
||||
);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
createFromExtensionsRedirects,
|
||||
createToExtensionsRedirects,
|
||||
} from '../extensionRedirects';
|
||||
import {RedirectMetadata} from '../types';
|
||||
|
||||
const createExtensionValidationTests = (
|
||||
extensionRedirectCreatorFn: (
|
||||
paths: string[],
|
||||
extensions: string[],
|
||||
) => RedirectMetadata[],
|
||||
) => {
|
||||
test('should reject empty extensions', () => {
|
||||
expect(() => {
|
||||
extensionRedirectCreatorFn(['/'], ['.html']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Extension=['.html'] contains a . (dot) and is not allowed. If the redirect extension system is not good enough for your usecase, you can create redirects yourself with the 'createRedirects' plugin option."`,
|
||||
);
|
||||
});
|
||||
test('should reject extensions with .', () => {
|
||||
expect(() => {
|
||||
extensionRedirectCreatorFn(['/'], ['.html']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Extension=['.html'] contains a . (dot) and is not allowed. If the redirect extension system is not good enough for your usecase, you can create redirects yourself with the 'createRedirects' plugin option."`,
|
||||
);
|
||||
});
|
||||
test('should reject extensions with /', () => {
|
||||
expect(() => {
|
||||
extensionRedirectCreatorFn(['/'], ['ht/ml']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Extension=['ht/ml'] contains a / and is not allowed. If the redirect extension system is not good enough for your usecase, you can create redirects yourself with the 'createRedirects' plugin option."`,
|
||||
);
|
||||
});
|
||||
test('should reject extensions with illegal url char', () => {
|
||||
expect(() => {
|
||||
extensionRedirectCreatorFn(['/'], [',']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Extension=[','] contains invalid uri characters. If the redirect extension system is not good enough for your usecase, you can create redirects yourself with the 'createRedirects' plugin option."`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
describe('createToExtensionsRedirects', () => {
|
||||
createExtensionValidationTests(createToExtensionsRedirects);
|
||||
|
||||
test('should create redirects from html/htm extensions', () => {
|
||||
const ext = ['html', 'htm'];
|
||||
expect(createToExtensionsRedirects([''], ext)).toEqual([]);
|
||||
expect(createToExtensionsRedirects(['/'], ext)).toEqual([]);
|
||||
expect(createToExtensionsRedirects(['/abc.html'], ext)).toEqual([
|
||||
{from: '/abc', to: '/abc.html'},
|
||||
]);
|
||||
expect(createToExtensionsRedirects(['/abc.htm'], ext)).toEqual([
|
||||
{from: '/abc', to: '/abc.htm'},
|
||||
]);
|
||||
expect(createToExtensionsRedirects(['/abc.xyz'], ext)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should not create redirection for an empty extension array', () => {
|
||||
const ext: string[] = [];
|
||||
expect(createToExtensionsRedirects([''], ext)).toEqual([]);
|
||||
expect(createToExtensionsRedirects(['/'], ext)).toEqual([]);
|
||||
expect(createToExtensionsRedirects(['/abc.html'], ext)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFromExtensionsRedirects', () => {
|
||||
createExtensionValidationTests(createFromExtensionsRedirects);
|
||||
|
||||
test('should create redirects to html/htm extensions', () => {
|
||||
const ext = ['html', 'htm'];
|
||||
expect(createFromExtensionsRedirects([''], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/'], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/abc'], ext)).toEqual([
|
||||
{from: '/abc.html', to: '/abc'},
|
||||
{from: '/abc.htm', to: '/abc'},
|
||||
]);
|
||||
expect(createFromExtensionsRedirects(['/def.html'], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/def/'], ext)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should not create redirection for an empty extension array', () => {
|
||||
const ext: string[] = [];
|
||||
expect(createFromExtensionsRedirects([''], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/'], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/abc'], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/def.html'], ext)).toEqual([]);
|
||||
expect(createFromExtensionsRedirects(['/def/'], ext)).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import normalizePluginOptions, {
|
||||
DefaultPluginOptions,
|
||||
} from '../normalizePluginOptions';
|
||||
import {CreateRedirectsFnOption} from '../types';
|
||||
|
||||
describe('normalizePluginOptions', () => {
|
||||
test('should return default options for undefined user options', () => {
|
||||
expect(normalizePluginOptions()).toEqual(DefaultPluginOptions);
|
||||
});
|
||||
|
||||
test('should return default options for empty user options', () => {
|
||||
expect(normalizePluginOptions()).toEqual(DefaultPluginOptions);
|
||||
});
|
||||
|
||||
test('should override one default options with valid user options', () => {
|
||||
expect(
|
||||
normalizePluginOptions({
|
||||
toExtensions: ['html'],
|
||||
}),
|
||||
).toEqual({...DefaultPluginOptions, toExtensions: ['html']});
|
||||
});
|
||||
|
||||
test('should override all default options with valid user options', () => {
|
||||
const createRedirects: CreateRedirectsFnOption = (_routePath: string) => {
|
||||
return [];
|
||||
};
|
||||
expect(
|
||||
normalizePluginOptions({
|
||||
fromExtensions: ['exe', 'zip'],
|
||||
toExtensions: ['html'],
|
||||
createRedirects,
|
||||
redirects: [{from: '/x', to: '/y'}],
|
||||
}),
|
||||
).toEqual({
|
||||
fromExtensions: ['exe', 'zip'],
|
||||
toExtensions: ['html'],
|
||||
createRedirects,
|
||||
redirects: [{from: '/x', to: '/y'}],
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject bad fromExtensions user inputs', () => {
|
||||
expect(() =>
|
||||
normalizePluginOptions({
|
||||
fromExtensions: [null, undefined, 123, true] as any,
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should reject bad toExtensions user inputs', () => {
|
||||
expect(() =>
|
||||
normalizePluginOptions({
|
||||
toExtensions: [null, undefined, 123, true] as any,
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should reject bad createRedirects user inputs', () => {
|
||||
expect(() =>
|
||||
normalizePluginOptions({
|
||||
createRedirects: ['bad', 'value'] as any,
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {validateRedirect} from '../redirectValidation';
|
||||
|
||||
describe('validateRedirect', () => {
|
||||
test('validate good redirects without throwing', () => {
|
||||
validateRedirect({
|
||||
from: '/fromSomePath',
|
||||
to: '/toSomePath',
|
||||
});
|
||||
validateRedirect({
|
||||
from: '/from/Some/Path',
|
||||
to: '/toSomePath',
|
||||
});
|
||||
validateRedirect({
|
||||
from: '/fromSomePath',
|
||||
to: '/toSomePath',
|
||||
});
|
||||
validateRedirect({
|
||||
from: '/fromSomePath',
|
||||
to: '/to/Some/Path',
|
||||
});
|
||||
});
|
||||
|
||||
test('throw for bad redirects', () => {
|
||||
expect(() =>
|
||||
validateRedirect({
|
||||
from: 'https://fb.com/fromSomePath',
|
||||
to: '/toSomePath',
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(() =>
|
||||
validateRedirect({
|
||||
from: '/fromSomePath',
|
||||
to: 'https://fb.com/toSomePath',
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(() =>
|
||||
validateRedirect({
|
||||
from: '/fromSomePath',
|
||||
to: '/toSomePath?queryString=xyz',
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(() =>
|
||||
validateRedirect({
|
||||
from: null as any,
|
||||
to: '/toSomePath?queryString=xyz',
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(() =>
|
||||
validateRedirect({
|
||||
from: ['heyho'] as any,
|
||||
to: '/toSomePath?queryString=xyz',
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import writeRedirectFiles, {
|
||||
toRedirectFilesMetadata,
|
||||
} from '../writeRedirectFiles';
|
||||
|
||||
describe('toRedirectFilesMetadata', () => {
|
||||
test('should create appropriate metadatas', async () => {
|
||||
const pluginContext = {
|
||||
outDir: '/tmp/someFixedOutDir',
|
||||
baseUrl: 'https://docusaurus.io',
|
||||
};
|
||||
|
||||
const redirectFiles = toRedirectFilesMetadata(
|
||||
[
|
||||
{from: '/abc.html', to: '/abc'},
|
||||
{from: '/def', to: '/def.html'},
|
||||
{from: '/xyz', to: '/'},
|
||||
],
|
||||
pluginContext,
|
||||
);
|
||||
|
||||
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
||||
path.join(pluginContext.outDir, '/abc.html/index.html'),
|
||||
path.join(pluginContext.outDir, '/def/index.html'),
|
||||
path.join(pluginContext.outDir, '/xyz/index.html'),
|
||||
]);
|
||||
|
||||
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||
'fileContent',
|
||||
);
|
||||
});
|
||||
|
||||
test('should create appropriate metadatas for root baseUrl', async () => {
|
||||
const pluginContext = {
|
||||
outDir: '/tmp/someFixedOutDir',
|
||||
baseUrl: '/',
|
||||
};
|
||||
const redirectFiles = toRedirectFilesMetadata(
|
||||
[{from: '/abc.html', to: '/abc'}],
|
||||
pluginContext,
|
||||
);
|
||||
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||
'fileContent baseUrl=/',
|
||||
);
|
||||
});
|
||||
|
||||
test('should create appropriate metadatas for empty baseUrl', async () => {
|
||||
const pluginContext = {
|
||||
outDir: '/tmp/someFixedOutDir',
|
||||
baseUrl: '',
|
||||
};
|
||||
const redirectFiles = toRedirectFilesMetadata(
|
||||
[{from: '/abc.html', to: '/abc'}],
|
||||
pluginContext,
|
||||
);
|
||||
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||
'fileContent baseUrl=empty',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeRedirectFiles', () => {
|
||||
test('write the files', async () => {
|
||||
const outDir = '/tmp/docusaurus_tests_' + Math.random();
|
||||
|
||||
const filesMetadata = [
|
||||
{
|
||||
fileAbsolutePath: path.join(outDir, 'someFileName'),
|
||||
fileContent: 'content 1',
|
||||
},
|
||||
{
|
||||
fileAbsolutePath: path.join(outDir, '/some/nested/filename'),
|
||||
fileContent: 'content 2',
|
||||
},
|
||||
];
|
||||
|
||||
await writeRedirectFiles(filesMetadata);
|
||||
|
||||
await expect(
|
||||
fs.readFile(filesMetadata[0].fileAbsolutePath, 'utf8'),
|
||||
).resolves.toEqual('content 1');
|
||||
|
||||
await expect(
|
||||
fs.readFile(filesMetadata[1].fileAbsolutePath, 'utf8'),
|
||||
).resolves.toEqual('content 2');
|
||||
});
|
||||
|
||||
test('avoid overwriting existing files', async () => {
|
||||
const outDir = '/tmp/docusaurus_tests_' + Math.random();
|
||||
|
||||
const filesMetadata = [
|
||||
{
|
||||
fileAbsolutePath: path.join(outDir, 'someFileName/index.html'),
|
||||
fileContent: 'content 1',
|
||||
},
|
||||
];
|
||||
|
||||
await fs.ensureDir(path.dirname(filesMetadata[0].fileAbsolutePath));
|
||||
await fs.writeFile(
|
||||
filesMetadata[0].fileAbsolutePath,
|
||||
'file already exists!',
|
||||
);
|
||||
|
||||
await expect(writeRedirectFiles(filesMetadata)).rejects.toThrowError(
|
||||
`Redirect file creation error for path=${filesMetadata[0].fileAbsolutePath}: Error: The redirect plugin is not supposed to override existing files`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {flatten, uniqBy, difference, groupBy} from 'lodash';
|
||||
import {
|
||||
PluginContext,
|
||||
RedirectMetadata,
|
||||
PluginOptions,
|
||||
RedirectOption,
|
||||
} from './types';
|
||||
import {
|
||||
createFromExtensionsRedirects,
|
||||
createToExtensionsRedirects,
|
||||
} from './extensionRedirects';
|
||||
import {validateRedirect} from './redirectValidation';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
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.to);
|
||||
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,
|
||||
): RedirectMetadata[] {
|
||||
// we don't want to create twice the same redirect
|
||||
// that would lead to writing twice the same html redirection file
|
||||
Object.entries(groupBy(redirects, (redirect) => redirect.from)).forEach(
|
||||
([from, groupedFromRedirects]) => {
|
||||
if (groupedFromRedirects.length > 1) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`@docusaurus/plugin-client-redirects: multiple redirects are created with the same "from" pathname=${from}
|
||||
It is not possible to redirect the same pathname to multiple destinations:
|
||||
- ${groupedFromRedirects.map((r) => JSON.stringify(r)).join('\n- ')}
|
||||
`,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
redirects = uniqBy(redirects, (redirect) => redirect.from);
|
||||
|
||||
// We don't want to override an already existing route with a redirect file!
|
||||
const redirectsOverridingExistingPath = redirects.filter((redirect) =>
|
||||
pluginContext.routesPaths.includes(redirect.from),
|
||||
);
|
||||
if (redirectsOverridingExistingPath.length > 0) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`@docusaurus/plugin-client-redirects: some redirects would override existing paths, and will be ignored:
|
||||
- ${redirectsOverridingExistingPath.map((r) => JSON.stringify(r)).join('\n- ')}
|
||||
`,
|
||||
),
|
||||
);
|
||||
}
|
||||
redirects = redirects.filter(
|
||||
(redirect) => !pluginContext.routesPaths.includes(redirect.from),
|
||||
);
|
||||
|
||||
return redirects;
|
||||
}
|
||||
|
||||
// For each plugin config option, create the appropriate redirects
|
||||
function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] {
|
||||
return [
|
||||
...createFromExtensionsRedirects(
|
||||
pluginContext.routesPaths,
|
||||
pluginContext.options.fromExtensions,
|
||||
),
|
||||
...createToExtensionsRedirects(
|
||||
pluginContext.routesPaths,
|
||||
pluginContext.options.toExtensions,
|
||||
),
|
||||
...createRedirectsOptionRedirects(pluginContext.options.redirects),
|
||||
...createCreateRedirectsOptionRedirects(
|
||||
pluginContext.routesPaths,
|
||||
pluginContext.options.createRedirects,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function createRedirectsOptionRedirects(
|
||||
redirectsOption: PluginOptions['redirects'],
|
||||
): RedirectMetadata[] {
|
||||
// For conveniency, user can use a string or a string[]
|
||||
function optionToRedirects(option: RedirectOption): RedirectMetadata[] {
|
||||
if (typeof option.from === 'string') {
|
||||
return [{from: option.from, to: option.to}];
|
||||
} else {
|
||||
return option.from.map((from) => ({
|
||||
from,
|
||||
to: option.to,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return flatten(redirectsOption.map(optionToRedirects));
|
||||
}
|
||||
|
||||
// Create redirects from the "createRedirects" fn provided by the user
|
||||
function createCreateRedirectsOptionRedirects(
|
||||
paths: string[],
|
||||
createRedirects: PluginOptions['createRedirects'],
|
||||
): RedirectMetadata[] {
|
||||
function createPathRedirects(path: string): RedirectMetadata[] {
|
||||
const fromsMixed: string | string[] = createRedirects
|
||||
? createRedirects(path) || []
|
||||
: [];
|
||||
|
||||
const froms: string[] =
|
||||
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
|
||||
|
||||
return froms.map((from) => {
|
||||
return {
|
||||
from,
|
||||
to: path,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return flatten(paths.map(createPathRedirects));
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import * as eta from 'eta';
|
||||
import redirectPageTemplate from './templates/redirectPage.template.html';
|
||||
|
||||
type CreateRedirectPageOptions = {
|
||||
toUrl: string;
|
||||
};
|
||||
|
||||
export default function createRedirectPageContent({
|
||||
toUrl,
|
||||
}: CreateRedirectPageOptions) {
|
||||
return eta.render(redirectPageTemplate.trim(), {
|
||||
toUrl: encodeURI(toUrl),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {flatten} from 'lodash';
|
||||
import {removeSuffix} from '@docusaurus/utils';
|
||||
import {RedirectMetadata} from './types';
|
||||
|
||||
const ExtensionAdditionalMessage =
|
||||
"If the redirect extension system is not good enough for your usecase, you can create redirects yourself with the 'createRedirects' plugin option.";
|
||||
|
||||
const validateExtension = (ext: string) => {
|
||||
if (!ext) {
|
||||
throw new Error(
|
||||
`Extension=['${String(
|
||||
ext,
|
||||
)}'] is not allowed. ${ExtensionAdditionalMessage}`,
|
||||
);
|
||||
}
|
||||
if (ext.includes('.')) {
|
||||
throw new Error(
|
||||
`Extension=['${ext}'] contains a . (dot) and is not allowed. ${ExtensionAdditionalMessage}`,
|
||||
);
|
||||
}
|
||||
if (ext.includes('/')) {
|
||||
throw new Error(
|
||||
`Extension=['${ext}'] contains a / and is not allowed. ${ExtensionAdditionalMessage}`,
|
||||
);
|
||||
}
|
||||
if (encodeURIComponent(ext) !== ext) {
|
||||
throw new Error(
|
||||
`Extension=['${ext}'] contains invalid uri characters. ${ExtensionAdditionalMessage}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addLeadingDot = (extension: string) => `.${extension}`;
|
||||
|
||||
// Create new /path that redirects to existing an /path.html
|
||||
export function createToExtensionsRedirects(
|
||||
paths: string[],
|
||||
extensions: string[],
|
||||
): RedirectMetadata[] {
|
||||
extensions.forEach(validateExtension);
|
||||
|
||||
const dottedExtensions = extensions.map(addLeadingDot);
|
||||
|
||||
const createPathRedirects = (path: string): RedirectMetadata[] => {
|
||||
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
|
||||
if (extensionFound) {
|
||||
const routePathWithoutExtension = removeSuffix(path, extensionFound);
|
||||
return [routePathWithoutExtension].map((from) => ({
|
||||
from: from,
|
||||
to: path,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return flatten(paths.map(createPathRedirects));
|
||||
}
|
||||
|
||||
// Create new /path.html that redirects to existing an /path
|
||||
export function createFromExtensionsRedirects(
|
||||
paths: string[],
|
||||
extensions: string[],
|
||||
): RedirectMetadata[] {
|
||||
extensions.forEach(validateExtension);
|
||||
|
||||
const dottedExtensions = extensions.map(addLeadingDot);
|
||||
|
||||
const alreadyEndsWithAnExtension = (str: string) =>
|
||||
dottedExtensions.some((ext) => str.endsWith(ext));
|
||||
|
||||
const createPathRedirects = (path: string): RedirectMetadata[] => {
|
||||
if (path === '' || path.endsWith('/') || alreadyEndsWithAnExtension(path)) {
|
||||
return [];
|
||||
} else {
|
||||
return extensions.map((ext) => ({
|
||||
from: `${path}.${ext}`,
|
||||
to: path,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return flatten(paths.map(createPathRedirects));
|
||||
}
|
45
packages/docusaurus-plugin-client-redirects/src/index.ts
Normal file
45
packages/docusaurus-plugin-client-redirects/src/index.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {LoadContext, Plugin, Props} from '@docusaurus/types';
|
||||
import {UserPluginOptions, PluginContext, RedirectMetadata} from './types';
|
||||
|
||||
import normalizePluginOptions from './normalizePluginOptions';
|
||||
import collectRedirects from './collectRedirects';
|
||||
import writeRedirectFiles, {
|
||||
toRedirectFilesMetadata,
|
||||
RedirectFileMetadata,
|
||||
} from './writeRedirectFiles';
|
||||
|
||||
export default function pluginClientRedirectsPages(
|
||||
_context: LoadContext,
|
||||
opts: UserPluginOptions,
|
||||
): Plugin<unknown> {
|
||||
const options = normalizePluginOptions(opts);
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-client-redirects',
|
||||
async postBuild(props: Props) {
|
||||
const pluginContext: PluginContext = {
|
||||
routesPaths: props.routesPaths,
|
||||
baseUrl: props.baseUrl,
|
||||
outDir: props.outDir,
|
||||
options,
|
||||
};
|
||||
|
||||
const redirects: RedirectMetadata[] = collectRedirects(pluginContext);
|
||||
|
||||
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
|
||||
redirects,
|
||||
pluginContext,
|
||||
);
|
||||
|
||||
// Write files only at the end: make code more easy to test without IO
|
||||
await writeRedirectFiles(redirectFiles);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
PluginOptions,
|
||||
RedirectOption,
|
||||
CreateRedirectsFnOption,
|
||||
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 CreateRedirectsFnOption | undefined {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
return value instanceof Function;
|
||||
}
|
||||
|
||||
const RedirectPluginOptionValidation = Yup.object<RedirectOption>({
|
||||
to: PathnameValidator.required(),
|
||||
// See https://stackoverflow.com/a/62177080/82609
|
||||
from: Yup.lazy<string | string[]>((from) => {
|
||||
return Array.isArray(from)
|
||||
? Yup.array().of(PathnameValidator.required()).required()
|
||||
: PathnameValidator.required();
|
||||
}),
|
||||
});
|
||||
|
||||
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',
|
||||
isRedirectsCreator,
|
||||
),
|
||||
});
|
||||
|
||||
function validateUserOptions(userOptions: UserPluginOptions) {
|
||||
try {
|
||||
UserOptionsSchema.validateSync(userOptions, {
|
||||
strict: true,
|
||||
abortEarly: true,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid @docusaurus/plugin-client-redirects options: ${e.message}
|
||||
${JSON.stringify(userOptions, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function normalizePluginOptions(
|
||||
userPluginOptions: UserPluginOptions = {},
|
||||
): PluginOptions {
|
||||
validateUserOptions(userPluginOptions);
|
||||
return {...DefaultPluginOptions, ...userPluginOptions};
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {isValidPathname} from '@docusaurus/utils';
|
||||
import * as Yup from 'yup';
|
||||
import {RedirectMetadata} from './types';
|
||||
|
||||
export const PathnameValidator = Yup.string().test({
|
||||
name: 'isValidPathname',
|
||||
message:
|
||||
'${path} is not a valid pathname. Pathname should start with / and not contain any domain or query string',
|
||||
test: isValidPathname,
|
||||
});
|
||||
|
||||
const RedirectSchema = Yup.object<RedirectMetadata>({
|
||||
from: PathnameValidator.required(),
|
||||
to: PathnameValidator.required(),
|
||||
});
|
||||
|
||||
export function validateRedirect(redirect: RedirectMetadata) {
|
||||
try {
|
||||
RedirectSchema.validateSync(redirect, {
|
||||
strict: true,
|
||||
abortEarly: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// Tells the user which redirect is the problem!
|
||||
throw new Error(
|
||||
`${JSON.stringify(redirect)} => Validation error: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export default `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0; url=<%= it.toUrl %>">
|
||||
<link rel="canonical" href="<%= it.toUrl %>" />
|
||||
</head>
|
||||
<script>
|
||||
window.location.href = '<%= it.toUrl %>';
|
||||
</script>
|
||||
</html>
|
||||
`;
|
44
packages/docusaurus-plugin-client-redirects/src/types.ts
Normal file
44
packages/docusaurus-plugin-client-redirects/src/types.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Props} from '@docusaurus/types';
|
||||
|
||||
export type PluginOptions = {
|
||||
fromExtensions: string[];
|
||||
toExtensions: string[];
|
||||
redirects: RedirectOption[];
|
||||
createRedirects?: CreateRedirectsFnOption;
|
||||
};
|
||||
|
||||
// For a given existing route path,
|
||||
// return all the paths from which we should redirect from
|
||||
export type CreateRedirectsFnOption = (
|
||||
path: string,
|
||||
) => string[] | string | null | undefined;
|
||||
|
||||
export type RedirectOption = {
|
||||
to: string;
|
||||
from: string | string[];
|
||||
};
|
||||
|
||||
export type UserPluginOptions = Partial<PluginOptions>;
|
||||
|
||||
// The minimal infos the plugin needs to work
|
||||
export type PluginContext = Pick<
|
||||
Props,
|
||||
'routesPaths' | 'outDir' | 'baseUrl'
|
||||
> & {
|
||||
options: PluginOptions;
|
||||
};
|
||||
|
||||
// In-memory representation of redirects we want: easier to test
|
||||
// /!\ easy to be confused: "from" is the new page we should create,
|
||||
// that redirects to "to": the existing Docusaurus page
|
||||
export type RedirectMetadata = {
|
||||
from: string; // pathname
|
||||
to: string; // pathname
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {memoize} from 'lodash';
|
||||
|
||||
import {PluginContext, RedirectMetadata} from './types';
|
||||
import createRedirectPageContent from './createRedirectPageContent';
|
||||
import {
|
||||
addTrailingSlash,
|
||||
getFilePathForRoutePath,
|
||||
removeTrailingSlash,
|
||||
} from '@docusaurus/utils';
|
||||
|
||||
export type WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>;
|
||||
|
||||
export type RedirectFileMetadata = {
|
||||
fileAbsolutePath: string;
|
||||
fileContent: string;
|
||||
};
|
||||
|
||||
export function toRedirectFilesMetadata(
|
||||
redirects: RedirectMetadata[],
|
||||
pluginContext: WriteFilesPluginContext,
|
||||
): RedirectFileMetadata[] {
|
||||
// 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!
|
||||
const createPageContentMemoized = memoize((toUrl: string) => {
|
||||
return createRedirectPageContent({toUrl});
|
||||
});
|
||||
|
||||
const createFileMetadata = (redirect: RedirectMetadata) => {
|
||||
const fileAbsolutePath = path.join(
|
||||
pluginContext.outDir,
|
||||
getFilePathForRoutePath(redirect.from),
|
||||
);
|
||||
const toUrl = addTrailingSlash(
|
||||
`${removeTrailingSlash(pluginContext.baseUrl)}${path.join(redirect.to)}`,
|
||||
);
|
||||
const fileContent = createPageContentMemoized(toUrl);
|
||||
return {
|
||||
...redirect,
|
||||
fileAbsolutePath,
|
||||
fileContent,
|
||||
};
|
||||
};
|
||||
|
||||
return redirects.map(createFileMetadata);
|
||||
}
|
||||
|
||||
export async function writeRedirectFile(file: RedirectFileMetadata) {
|
||||
try {
|
||||
// User-friendly security to prevent file overrides
|
||||
if (await fs.pathExists(file.fileAbsolutePath)) {
|
||||
throw new Error(
|
||||
'The redirect plugin is not supposed to override existing files',
|
||||
);
|
||||
}
|
||||
await fs.ensureDir(path.dirname(file.fileAbsolutePath));
|
||||
await fs.writeFile(
|
||||
file.fileAbsolutePath,
|
||||
file.fileContent,
|
||||
// Hard security to prevent file overrides
|
||||
// See https://stackoverflow.com/a/34187712/82609
|
||||
{flag: 'wx'},
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Redirect file creation error for path=${file.fileAbsolutePath}: ${err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function writeRedirectFiles(
|
||||
redirectFiles: RedirectFileMetadata[],
|
||||
) {
|
||||
await Promise.all(redirectFiles.map(writeRedirectFile));
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
}
|
||||
}
|
|
@ -18,6 +18,11 @@ import {
|
|||
objectWithKeySorted,
|
||||
aliasedSitePath,
|
||||
createExcerpt,
|
||||
isValidPathname,
|
||||
addTrailingSlash,
|
||||
removeTrailingSlash,
|
||||
removeSuffix,
|
||||
getFilePathForRoutePath,
|
||||
} from '../index';
|
||||
|
||||
describe('load utils', () => {
|
||||
|
@ -363,4 +368,69 @@ describe('load utils', () => {
|
|||
expect(createExcerpt(testCase.input)).toEqual(testCase.output);
|
||||
});
|
||||
});
|
||||
|
||||
test('isValidPathname', () => {
|
||||
expect(isValidPathname('/')).toBe(true);
|
||||
expect(isValidPathname('/hey')).toBe(true);
|
||||
expect(isValidPathname('/hey/ho')).toBe(true);
|
||||
expect(isValidPathname('/hey/ho/')).toBe(true);
|
||||
expect(isValidPathname('/hey/h%C3%B4/')).toBe(true);
|
||||
expect(isValidPathname('/hey///ho///')).toBe(true); // Unexpected but valid
|
||||
//
|
||||
expect(isValidPathname('')).toBe(false);
|
||||
expect(isValidPathname('hey')).toBe(false);
|
||||
expect(isValidPathname('/hey/hô')).toBe(false);
|
||||
expect(isValidPathname('/hey?qs=ho')).toBe(false);
|
||||
expect(isValidPathname('https://fb.com/hey')).toBe(false);
|
||||
expect(isValidPathname('//hey')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addTrailingSlash', () => {
|
||||
test('should no-op', () => {
|
||||
expect(addTrailingSlash('/abcd/')).toEqual('/abcd/');
|
||||
});
|
||||
test('should add /', () => {
|
||||
expect(addTrailingSlash('/abcd')).toEqual('/abcd/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTrailingSlash', () => {
|
||||
test('should no-op', () => {
|
||||
expect(removeTrailingSlash('/abcd')).toEqual('/abcd');
|
||||
});
|
||||
test('should remove /', () => {
|
||||
expect(removeTrailingSlash('/abcd/')).toEqual('/abcd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSuffix', () => {
|
||||
test('should no-op 1', () => {
|
||||
expect(removeSuffix('abcdef', 'ijk')).toEqual('abcdef');
|
||||
});
|
||||
test('should no-op 2', () => {
|
||||
expect(removeSuffix('abcdef', 'abc')).toEqual('abcdef');
|
||||
});
|
||||
test('should no-op 3', () => {
|
||||
expect(removeSuffix('abcdef', '')).toEqual('abcdef');
|
||||
});
|
||||
test('should remove suffix', () => {
|
||||
expect(removeSuffix('abcdef', 'ef')).toEqual('abcd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilePathForRoutePath', () => {
|
||||
test('works for /', () => {
|
||||
expect(getFilePathForRoutePath('/')).toEqual('/index.html');
|
||||
});
|
||||
test('works for /somePath', () => {
|
||||
expect(getFilePathForRoutePath('/somePath')).toEqual(
|
||||
'/somePath/index.html',
|
||||
);
|
||||
});
|
||||
test('works for /somePath/', () => {
|
||||
expect(getFilePathForRoutePath('/somePath/')).toEqual(
|
||||
'/somePath/index.html',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import camelCase from 'lodash.camelcase';
|
|||
import kebabCase from 'lodash.kebabcase';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
import fs from 'fs-extra';
|
||||
import {URL} from 'url';
|
||||
|
||||
const fileHash = new Map();
|
||||
export async function generate(
|
||||
|
@ -349,3 +350,35 @@ export function getEditUrl(fileRelativePath: string, editUrl?: string) {
|
|||
? normalizeUrl([editUrl, posixPath(fileRelativePath)])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function isValidPathname(str: string): boolean {
|
||||
if (!str.startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new URL(str, 'https://domain.com').pathname === str;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function addTrailingSlash(str: string) {
|
||||
return str.endsWith('/') ? str : `${str}/`;
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(str: string) {
|
||||
return removeSuffix(str, '/');
|
||||
}
|
||||
|
||||
export function removeSuffix(str: string, suffix: string) {
|
||||
if (suffix === '') {
|
||||
return str; // always returns "" otherwise!
|
||||
}
|
||||
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
|
||||
}
|
||||
|
||||
export function getFilePathForRoutePath(routePath: string) {
|
||||
const fileName = path.basename(routePath);
|
||||
const filePath = path.dirname(routePath);
|
||||
return path.join(filePath, `${fileName}/index.html`);
|
||||
}
|
||||
|
|
|
@ -448,6 +448,43 @@ The following fields are all deprecated, you may remove from your configuration
|
|||
|
||||
We intend to implement many of the deprecated config fields as plugins in future. Help will be appreciated!
|
||||
|
||||
## Urls
|
||||
|
||||
In v1, all pages were available with or without the `.html` extension.
|
||||
|
||||
For example, these 2 pages exist:
|
||||
|
||||
- [https://docusaurus.io/docs/en/installation](https://docusaurus.io/docs/en/installation)
|
||||
- [https://docusaurus.io/docs/en/installation.html](https://docusaurus.io/docs/en/installation.html)
|
||||
|
||||
If [`cleanUrl`](https://docusaurus.io/docs/en/site-config#cleanurl-boolean) was:
|
||||
|
||||
- `true`: links would target `/installation`
|
||||
- `false`: links would target `/installation.html`
|
||||
|
||||
In v2, by default, the canonical page is `/installation`, and not `/installation.html`.
|
||||
|
||||
If you had `cleanUrl: false` in v1, it's possible that people published links to `/installation.html`.
|
||||
|
||||
For SEO reasons, and avoiding breaking links, you should configure server-side redirect rules on your hosting provider.
|
||||
|
||||
As an escape hatch, you could use [@docusaurus/plugin-client-redirects](./using-plugins.md#docusaurusplugin-client-redirects) to create client-side redirects from `/installation.html` to `/installation`.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
fromExtensions: ['html'],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
If you want to keep the `.html` extension as the canonical url of a page, docs can declare a `slug: installation.html` frontmatter.
|
||||
|
||||
## Components
|
||||
|
||||
### Sidebar
|
||||
|
|
|
@ -458,3 +458,123 @@ import thumbnail from './path/to/img.png';
|
|||
| `max` | `integer` | | See `min` above |
|
||||
| `steps` | `integer` | `4` | Configure the number of images generated between `min` and `max` (inclusive) |
|
||||
| `quality` | `integer` | `85` | JPEG compression quality |
|
||||
|
||||
### `@docusaurus/plugin-client-redirects`
|
||||
|
||||
Docusaurus Plugin to generate **client-side redirects**.
|
||||
|
||||
This plugin will write additional HTML pages to your static site, that redirects the user to your existing Docusaurus pages with JavaScript.
|
||||
|
||||
:::caution
|
||||
|
||||
It is better to use server-side redirects whenever possible.
|
||||
|
||||
Before using this plugin, you should look if your hosting provider doesn't offer this feature.
|
||||
|
||||
:::
|
||||
|
||||
**Installation**
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save @docusaurus/plugin-client-redirects
|
||||
```
|
||||
|
||||
**Configuration**
|
||||
|
||||
Main usecase: you have `/myDocusaurusPage`, and you want to redirect to this page from `/myDocusaurusPage.html`:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
fromExtensions: ['html'],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Second usecase: you have `/myDocusaurusPage.html`, and you want to redirect to this page from `/myDocusaurusPage`.
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
toExtensions: ['html'],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
For custom redirect logic, provide your own `createRedirects` function.
|
||||
|
||||
Let's imagine you change the url of an existing page, you might want to make sure the old url still works:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
redirects: [
|
||||
{
|
||||
to: '/docs/newDocPath', // string
|
||||
from: ['/docs/oldDocPathFrom2019', '/docs/legacyDocPathFrom2016'], // string | string[]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
It's possible to use a function to create the redirects for each existing path:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
createRedirects: function (existingPath) {
|
||||
if (existingPath === '/docs/newDocPath') {
|
||||
return ['/docs/oldDocPathFrom2019', '/docs/legacyDocPathFrom2016']; // string | string[]
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Finally, it's possible to use all options at the same time:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
fromExtensions: ['html', 'htm'],
|
||||
toExtensions: ['exe', 'zip'],
|
||||
redirects: [
|
||||
{
|
||||
to: '/docs/newDocPath',
|
||||
from: '/docs/oldDocPath',
|
||||
},
|
||||
],
|
||||
createRedirects: function (existingPath) {
|
||||
if (existingPath === '/docs/newDocPath2') {
|
||||
return ['/docs/oldDocPath2'];
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
const versions = require('./versions.json');
|
||||
|
||||
const allDocHomesPaths = [
|
||||
'/docs',
|
||||
'/docs/next',
|
||||
...versions.slice(1).map((version) => `/docs/${version}`),
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
title: 'Docusaurus',
|
||||
tagline: 'Build optimized websites quickly, focus on your content',
|
||||
|
@ -21,6 +27,19 @@ module.exports = {
|
|||
},
|
||||
themes: ['@docusaurus/theme-live-codeblock'],
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
fromExtensions: ['html'],
|
||||
createRedirects: function (path) {
|
||||
// redirect to /docs from /docs/introduction,
|
||||
// as introduction has been made the home doc
|
||||
if (allDocHomesPaths.includes(path)) {
|
||||
return [`${path}/introduction`];
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'@docusaurus/plugin-ideal-image',
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.56",
|
||||
"@docusaurus/plugin-ideal-image": "^2.0.0-alpha.56",
|
||||
"@docusaurus/plugin-client-redirects": "^2.0.0-alpha.56",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.56",
|
||||
"@docusaurus/theme-live-codeblock": "^2.0.0-alpha.56",
|
||||
"clsx": "^1.1.1",
|
||||
|
|
50
yarn.lock
50
yarn.lock
|
@ -1156,6 +1156,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
|
||||
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
|
@ -3135,6 +3142,11 @@
|
|||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yup@^0.29.0":
|
||||
version "0.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.0.tgz#0918ec503dfacb19d0b3cca0195b9f3441f46685"
|
||||
integrity sha512-E9RTXPD4x44qBOvY6TjUqdkR9FNV9cACWlnAsooUInDqtLZz9M9oYXKn/w1GHNxRvyYyHuG6Bfjbg3QlK+SgXw==
|
||||
|
||||
"@webassemblyjs/ast@1.8.5":
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
|
||||
|
@ -7658,6 +7670,11 @@ flush-write-stream@^1.0.0:
|
|||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
fn-name@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
|
||||
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
|
||||
|
@ -10720,6 +10737,11 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash-es@^4.17.11:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
|
||||
lodash._reinterpolate@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||
|
@ -13936,6 +13958,11 @@ prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.4, prop-types@^15.5.8,
|
|||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
property-expr@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
|
||||
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
|
||||
|
||||
property-information@^5.0.0, property-information@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.3.0.tgz#bc87ac82dc4e72a31bb62040544b1bf9653da039"
|
||||
|
@ -16483,6 +16510,11 @@ symbol-tree@^3.2.2:
|
|||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
synchronous-promise@^2.0.10:
|
||||
version "2.0.13"
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
|
||||
integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
|
||||
|
||||
table@^5.2.3, table@^5.4.6:
|
||||
version "5.4.6"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
||||
|
@ -16845,6 +16877,11 @@ toml@^2.3.2:
|
|||
resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b"
|
||||
integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==
|
||||
|
||||
toposort@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
|
||||
|
||||
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
|
@ -18177,6 +18214,19 @@ yauzl@^2.4.2:
|
|||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yup@^0.29.0:
|
||||
version "0.29.0"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.0.tgz#c0670897b2ebcea42ebde12b3567f55ea3a7acaf"
|
||||
integrity sha512-rXPkxhMIVPsQ6jZXPRcO+nc+AIT+BBo3012pmiEos2RSrPxAq1LyspZyK7l14ahcXuiKQnEHI0H5bptI47v5Tw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.6"
|
||||
fn-name "~3.0.0"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.11"
|
||||
property-expr "^2.0.2"
|
||||
synchronous-promise "^2.0.10"
|
||||
toposort "^2.0.2"
|
||||
|
||||
zepto@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue