@docusaurus/plugin-client-redirects 2.0.0-beta.21 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/collectRedirects.d.ts +2 -2
- package/lib/collectRedirects.js +29 -41
- package/lib/extensionRedirects.d.ts +11 -3
- package/lib/extensionRedirects.js +9 -8
- package/lib/index.js +2 -1
- package/lib/options.d.ts +5 -1
- package/lib/redirectValidation.d.ts +2 -2
- package/lib/types.d.ts +2 -2
- package/lib/writeRedirectFiles.d.ts +5 -5
- package/lib/writeRedirectFiles.js +3 -3
- package/package.json +8 -8
- package/src/collectRedirects.ts +55 -73
- package/src/deps.d.ts +16 -0
- package/src/extensionRedirects.ts +14 -13
- package/src/index.ts +6 -5
- package/src/options.ts +5 -1
- package/src/redirectValidation.ts +3 -3
- package/src/types.ts +2 -2
- package/src/writeRedirectFiles.ts +8 -10
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import type { PluginContext,
|
|
8
|
-
export default function collectRedirects(pluginContext: PluginContext, trailingSlash: boolean | undefined):
|
|
7
|
+
import type { PluginContext, RedirectItem } from './types';
|
|
8
|
+
export default function collectRedirects(pluginContext: PluginContext, trailingSlash: boolean | undefined): RedirectItem[];
|
package/lib/collectRedirects.js
CHANGED
|
@@ -13,28 +13,32 @@ const utils_common_1 = require("@docusaurus/utils-common");
|
|
|
13
13
|
const extensionRedirects_1 = require("./extensionRedirects");
|
|
14
14
|
const redirectValidation_1 = require("./redirectValidation");
|
|
15
15
|
function collectRedirects(pluginContext, trailingSlash) {
|
|
16
|
-
|
|
17
|
-
redirects =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// For each plugin config option, create the appropriate redirects
|
|
17
|
+
const redirects = [
|
|
18
|
+
...(0, extensionRedirects_1.createFromExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.fromExtensions),
|
|
19
|
+
...(0, extensionRedirects_1.createToExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.toExtensions),
|
|
20
|
+
...createRedirectsOptionRedirects(pluginContext.options.redirects),
|
|
21
|
+
...createCreateRedirectsOptionRedirects(pluginContext.relativeRoutesPaths, pluginContext.options.createRedirects),
|
|
22
|
+
].map((redirect) => ({
|
|
23
|
+
...redirect,
|
|
24
|
+
// Given a redirect with `to: "/abc"` and `trailingSlash` enabled:
|
|
25
|
+
//
|
|
26
|
+
// - We don't want to reject `to: "/abc"`, as that unambiguously points to
|
|
27
|
+
// `/abc/` now;
|
|
28
|
+
// - We want to redirect `to: /abc/` without the user having to change all
|
|
29
|
+
// her redirect plugin options
|
|
30
|
+
//
|
|
31
|
+
// It should be easy to toggle `trailingSlash` option without having to
|
|
32
|
+
// change other configs
|
|
33
|
+
to: (0, utils_common_1.applyTrailingSlash)(redirect.to, {
|
|
34
|
+
trailingSlash,
|
|
35
|
+
baseUrl: pluginContext.baseUrl,
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
21
38
|
validateCollectedRedirects(redirects, pluginContext);
|
|
22
39
|
return filterUnwantedRedirects(redirects, pluginContext);
|
|
23
40
|
}
|
|
24
41
|
exports.default = collectRedirects;
|
|
25
|
-
// If users wants to redirect to=/abc and they enable trailingSlash=true then
|
|
26
|
-
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid
|
|
27
|
-
// path now)
|
|
28
|
-
// => we want to redirect to=/abc/ without the user having to change all its
|
|
29
|
-
// redirect plugin options
|
|
30
|
-
// It should be easy to toggle siteConfig.trailingSlash option without having to
|
|
31
|
-
// change other configs
|
|
32
|
-
function applyRedirectsTrailingSlash(redirects, params) {
|
|
33
|
-
return redirects.map((redirect) => ({
|
|
34
|
-
...redirect,
|
|
35
|
-
to: (0, utils_common_1.applyTrailingSlash)(redirect.to, params),
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
42
|
function validateCollectedRedirects(redirects, pluginContext) {
|
|
39
43
|
const redirectValidationErrors = redirects
|
|
40
44
|
.map((redirect) => {
|
|
@@ -69,26 +73,16 @@ function filterUnwantedRedirects(redirects, pluginContext) {
|
|
|
69
73
|
// writing the same html redirection file twice.
|
|
70
74
|
Object.entries(lodash_1.default.groupBy(redirects, (redirect) => redirect.from)).forEach(([from, groupedFromRedirects]) => {
|
|
71
75
|
if (groupedFromRedirects.length > 1) {
|
|
72
|
-
logger_1.default.
|
|
73
|
-
It is not possible to redirect the same pathname to multiple destinations
|
|
76
|
+
logger_1.default.report(pluginContext.siteConfig.onDuplicateRoutes) `name=${'@docusaurus/plugin-client-redirects'}: multiple redirects are created with the same "from" pathname: path=${from}
|
|
77
|
+
It is not possible to redirect the same pathname to multiple destinations:${groupedFromRedirects.map((r) => JSON.stringify(r))}`;
|
|
74
78
|
}
|
|
75
79
|
});
|
|
76
80
|
const collectedRedirects = lodash_1.default.uniqBy(redirects, (redirect) => redirect.from);
|
|
77
|
-
|
|
78
|
-
const redirectsOverridingExistingPath = collectedRedirects.filter((redirect) => pluginContext.relativeRoutesPaths.includes(redirect.from));
|
|
81
|
+
const { false: newRedirects = [], true: redirectsOverridingExistingPath = [] } = lodash_1.default.groupBy(collectedRedirects, (redirect) => pluginContext.relativeRoutesPaths.includes(redirect.from));
|
|
79
82
|
if (redirectsOverridingExistingPath.length > 0) {
|
|
80
|
-
logger_1.default.
|
|
83
|
+
logger_1.default.report(pluginContext.siteConfig.onDuplicateRoutes) `name=${'@docusaurus/plugin-client-redirects'}: some redirects would override existing paths, and will be ignored:${redirectsOverridingExistingPath.map((r) => JSON.stringify(r))}`;
|
|
81
84
|
}
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
// For each plugin config option, create the appropriate redirects
|
|
85
|
-
function doCollectRedirects(pluginContext) {
|
|
86
|
-
return [
|
|
87
|
-
...(0, extensionRedirects_1.createFromExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.fromExtensions),
|
|
88
|
-
...(0, extensionRedirects_1.createToExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.toExtensions),
|
|
89
|
-
...createRedirectsOptionRedirects(pluginContext.options.redirects),
|
|
90
|
-
...createCreateRedirectsOptionRedirects(pluginContext.relativeRoutesPaths, pluginContext.options.createRedirects),
|
|
91
|
-
];
|
|
85
|
+
return newRedirects;
|
|
92
86
|
}
|
|
93
87
|
function createRedirectsOptionRedirects(redirectsOption) {
|
|
94
88
|
// For convenience, user can use a string or a string[]
|
|
@@ -96,10 +90,7 @@ function createRedirectsOptionRedirects(redirectsOption) {
|
|
|
96
90
|
if (typeof option.from === 'string') {
|
|
97
91
|
return [{ from: option.from, to: option.to }];
|
|
98
92
|
}
|
|
99
|
-
return option.from.map((from) => ({
|
|
100
|
-
from,
|
|
101
|
-
to: option.to,
|
|
102
|
-
}));
|
|
93
|
+
return option.from.map((from) => ({ from, to: option.to }));
|
|
103
94
|
}
|
|
104
95
|
return redirectsOption.flatMap(optionToRedirects);
|
|
105
96
|
}
|
|
@@ -108,10 +99,7 @@ function createCreateRedirectsOptionRedirects(paths, createRedirects) {
|
|
|
108
99
|
function createPathRedirects(path) {
|
|
109
100
|
const fromsMixed = createRedirects?.(path) ?? [];
|
|
110
101
|
const froms = typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
|
|
111
|
-
return froms.map((from) => ({
|
|
112
|
-
from,
|
|
113
|
-
to: path,
|
|
114
|
-
}));
|
|
102
|
+
return froms.map((from) => ({ from, to: path }));
|
|
115
103
|
}
|
|
116
104
|
return paths.flatMap(createPathRedirects);
|
|
117
105
|
}
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import type { RedirectItem } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Create new `/path` that redirects to existing an `/path.html`
|
|
10
|
+
*/
|
|
11
|
+
export declare function createToExtensionsRedirects(paths: string[], extensions: string[]): RedirectItem[];
|
|
12
|
+
/**
|
|
13
|
+
* Create new `/path.html/index.html` that redirects to existing an `/path`
|
|
14
|
+
* The filename pattern might look weird but it's on purpose (see
|
|
15
|
+
* https://github.com/facebook/docusaurus/issues/5055)
|
|
16
|
+
*/
|
|
17
|
+
export declare function createFromExtensionsRedirects(paths: string[], extensions: string[]): RedirectItem[];
|
|
@@ -24,26 +24,27 @@ const validateExtension = (ext) => {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
const addLeadingDot = (extension) => `.${extension}`;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Create new `/path` that redirects to existing an `/path.html`
|
|
29
|
+
*/
|
|
28
30
|
function createToExtensionsRedirects(paths, extensions) {
|
|
29
31
|
extensions.forEach(validateExtension);
|
|
30
32
|
const dottedExtensions = extensions.map(addLeadingDot);
|
|
31
33
|
const createPathRedirects = (path) => {
|
|
32
34
|
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
|
|
33
35
|
if (extensionFound) {
|
|
34
|
-
|
|
35
|
-
return [routePathWithoutExtension].map((from) => ({
|
|
36
|
-
from,
|
|
37
|
-
to: path,
|
|
38
|
-
}));
|
|
36
|
+
return [{ from: (0, utils_1.removeSuffix)(path, extensionFound), to: path }];
|
|
39
37
|
}
|
|
40
38
|
return [];
|
|
41
39
|
};
|
|
42
40
|
return paths.flatMap(createPathRedirects);
|
|
43
41
|
}
|
|
44
42
|
exports.createToExtensionsRedirects = createToExtensionsRedirects;
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Create new `/path.html/index.html` that redirects to existing an `/path`
|
|
45
|
+
* The filename pattern might look weird but it's on purpose (see
|
|
46
|
+
* https://github.com/facebook/docusaurus/issues/5055)
|
|
47
|
+
*/
|
|
47
48
|
function createFromExtensionsRedirects(paths, extensions) {
|
|
48
49
|
extensions.forEach(validateExtension);
|
|
49
50
|
const dottedExtensions = extensions.map(addLeadingDot);
|
package/lib/index.js
CHANGED
|
@@ -21,9 +21,10 @@ function pluginClientRedirectsPages(context, options) {
|
|
|
21
21
|
baseUrl: props.baseUrl,
|
|
22
22
|
outDir: props.outDir,
|
|
23
23
|
options,
|
|
24
|
+
siteConfig: props.siteConfig,
|
|
24
25
|
};
|
|
25
26
|
const redirects = (0, collectRedirects_1.default)(pluginContext, trailingSlash);
|
|
26
|
-
const redirectFiles = (0, writeRedirectFiles_1.
|
|
27
|
+
const redirectFiles = (0, writeRedirectFiles_1.toRedirectFiles)(redirects, pluginContext, trailingSlash);
|
|
27
28
|
// Write files only at the end: make code more easy to test without IO
|
|
28
29
|
await (0, writeRedirectFiles_1.default)(redirectFiles);
|
|
29
30
|
},
|
package/lib/options.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { OptionValidationContext } from '@docusaurus/types';
|
|
8
8
|
export declare type RedirectOption = {
|
|
9
|
+
/** Pathname of an existing Docusaurus page */
|
|
9
10
|
to: string;
|
|
11
|
+
/** Pathname of the new page(s) we should create */
|
|
10
12
|
from: string | string[];
|
|
11
13
|
};
|
|
12
14
|
export declare type PluginOptions = {
|
|
@@ -19,7 +21,9 @@ export declare type PluginOptions = {
|
|
|
19
21
|
/** The list of redirect rules, each one with multiple `from`s → one `to`. */
|
|
20
22
|
redirects: RedirectOption[];
|
|
21
23
|
/**
|
|
22
|
-
* A callback to create a redirect rule.
|
|
24
|
+
* A callback to create a redirect rule. Docusaurus query this callback
|
|
25
|
+
* against every path it has created, and use its return value to output more
|
|
26
|
+
* paths.
|
|
23
27
|
* @returns All the paths from which we should redirect to `path`
|
|
24
28
|
*/
|
|
25
29
|
createRedirects?: (
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
export declare function validateRedirect(redirect:
|
|
7
|
+
import type { RedirectItem } from './types';
|
|
8
|
+
export declare function validateRedirect(redirect: RedirectItem): void;
|
package/lib/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { PluginOptions } from './options';
|
|
|
9
9
|
/**
|
|
10
10
|
* The minimal infos the plugin needs to work
|
|
11
11
|
*/
|
|
12
|
-
export declare type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
|
|
12
|
+
export declare type PluginContext = Pick<Props, 'outDir' | 'baseUrl' | 'siteConfig'> & {
|
|
13
13
|
options: PluginOptions;
|
|
14
14
|
relativeRoutesPaths: string[];
|
|
15
15
|
};
|
|
@@ -18,7 +18,7 @@ export declare type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
|
|
|
18
18
|
* /!\ easy to be confused: "from" is the new page we should create,
|
|
19
19
|
* that redirects to "to": the existing Docusaurus page
|
|
20
20
|
*/
|
|
21
|
-
export declare type
|
|
21
|
+
export declare type RedirectItem = {
|
|
22
22
|
/** Pathname of the new page we should create */
|
|
23
23
|
from: string;
|
|
24
24
|
/** Pathname of an existing Docusaurus page */
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import type { PluginContext,
|
|
7
|
+
import type { PluginContext, RedirectItem } from './types';
|
|
8
8
|
export declare type WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>;
|
|
9
|
-
export declare type
|
|
9
|
+
export declare type RedirectFile = {
|
|
10
10
|
fileAbsolutePath: string;
|
|
11
11
|
fileContent: string;
|
|
12
12
|
};
|
|
13
13
|
export declare function createToUrl(baseUrl: string, to: string): string;
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function writeRedirectFile(file:
|
|
16
|
-
export default function writeRedirectFiles(redirectFiles:
|
|
14
|
+
export declare function toRedirectFiles(redirects: RedirectItem[], pluginContext: WriteFilesPluginContext, trailingSlash: boolean | undefined): RedirectFile[];
|
|
15
|
+
export declare function writeRedirectFile(file: RedirectFile): Promise<void>;
|
|
16
|
+
export default function writeRedirectFiles(redirectFiles: RedirectFile[]): Promise<void>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.writeRedirectFile = exports.
|
|
9
|
+
exports.writeRedirectFile = exports.toRedirectFiles = exports.createToUrl = void 0;
|
|
10
10
|
const tslib_1 = require("tslib");
|
|
11
11
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
12
12
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
@@ -45,7 +45,7 @@ function getRedirectFilePath(fromPath, trailingSlash) {
|
|
|
45
45
|
// tools
|
|
46
46
|
return path_1.default.join(filePath, `${fileName}/index.html`);
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
function toRedirectFiles(redirects, pluginContext, trailingSlash) {
|
|
49
49
|
// Perf: avoid rendering the template twice with the exact same "props"
|
|
50
50
|
// We might create multiple redirect pages for the same destination url
|
|
51
51
|
// note: the first fn arg is the cache key!
|
|
@@ -63,7 +63,7 @@ function toRedirectFilesMetadata(redirects, pluginContext, trailingSlash) {
|
|
|
63
63
|
};
|
|
64
64
|
return redirects.map(createFileMetadata);
|
|
65
65
|
}
|
|
66
|
-
exports.
|
|
66
|
+
exports.toRedirectFiles = toRedirectFiles;
|
|
67
67
|
async function writeRedirectFile(file) {
|
|
68
68
|
try {
|
|
69
69
|
// User-friendly security to prevent file overrides
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docusaurus/plugin-client-redirects",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"description": "Client redirects plugin for Docusaurus.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@docusaurus/core": "2.0.0-beta.
|
|
22
|
-
"@docusaurus/logger": "2.0.0-beta.
|
|
23
|
-
"@docusaurus/utils": "2.0.0-beta.
|
|
24
|
-
"@docusaurus/utils-common": "2.0.0-beta.
|
|
25
|
-
"@docusaurus/utils-validation": "2.0.0-beta.
|
|
21
|
+
"@docusaurus/core": "2.0.0-beta.22",
|
|
22
|
+
"@docusaurus/logger": "2.0.0-beta.22",
|
|
23
|
+
"@docusaurus/utils": "2.0.0-beta.22",
|
|
24
|
+
"@docusaurus/utils-common": "2.0.0-beta.22",
|
|
25
|
+
"@docusaurus/utils-validation": "2.0.0-beta.22",
|
|
26
26
|
"eta": "^1.12.3",
|
|
27
27
|
"fs-extra": "^10.1.0",
|
|
28
28
|
"lodash": "^4.17.21",
|
|
29
29
|
"tslib": "^2.4.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@docusaurus/types": "2.0.0-beta.
|
|
32
|
+
"@docusaurus/types": "2.0.0-beta.22"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^16.8.4 || ^17.0.0",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=16.14"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "daf9e462c4eebb7ac26a940932311f987e768f87"
|
|
42
42
|
}
|
package/src/collectRedirects.ts
CHANGED
|
@@ -7,52 +7,57 @@
|
|
|
7
7
|
|
|
8
8
|
import _ from 'lodash';
|
|
9
9
|
import logger from '@docusaurus/logger';
|
|
10
|
-
import {
|
|
11
|
-
applyTrailingSlash,
|
|
12
|
-
type ApplyTrailingSlashParams,
|
|
13
|
-
} from '@docusaurus/utils-common';
|
|
10
|
+
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
|
14
11
|
import {
|
|
15
12
|
createFromExtensionsRedirects,
|
|
16
13
|
createToExtensionsRedirects,
|
|
17
14
|
} from './extensionRedirects';
|
|
18
15
|
import {validateRedirect} from './redirectValidation';
|
|
19
16
|
import type {PluginOptions, RedirectOption} from './options';
|
|
20
|
-
import type {PluginContext,
|
|
17
|
+
import type {PluginContext, RedirectItem} from './types';
|
|
21
18
|
|
|
22
19
|
export default function collectRedirects(
|
|
23
20
|
pluginContext: PluginContext,
|
|
24
21
|
trailingSlash: boolean | undefined,
|
|
25
|
-
):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
): RedirectItem[] {
|
|
23
|
+
// For each plugin config option, create the appropriate redirects
|
|
24
|
+
const redirects = [
|
|
25
|
+
...createFromExtensionsRedirects(
|
|
26
|
+
pluginContext.relativeRoutesPaths,
|
|
27
|
+
pluginContext.options.fromExtensions,
|
|
28
|
+
),
|
|
29
|
+
...createToExtensionsRedirects(
|
|
30
|
+
pluginContext.relativeRoutesPaths,
|
|
31
|
+
pluginContext.options.toExtensions,
|
|
32
|
+
),
|
|
33
|
+
...createRedirectsOptionRedirects(pluginContext.options.redirects),
|
|
34
|
+
...createCreateRedirectsOptionRedirects(
|
|
35
|
+
pluginContext.relativeRoutesPaths,
|
|
36
|
+
pluginContext.options.createRedirects,
|
|
37
|
+
),
|
|
38
|
+
].map((redirect) => ({
|
|
39
|
+
...redirect,
|
|
40
|
+
// Given a redirect with `to: "/abc"` and `trailingSlash` enabled:
|
|
41
|
+
//
|
|
42
|
+
// - We don't want to reject `to: "/abc"`, as that unambiguously points to
|
|
43
|
+
// `/abc/` now;
|
|
44
|
+
// - We want to redirect `to: /abc/` without the user having to change all
|
|
45
|
+
// her redirect plugin options
|
|
46
|
+
//
|
|
47
|
+
// It should be easy to toggle `trailingSlash` option without having to
|
|
48
|
+
// change other configs
|
|
49
|
+
to: applyTrailingSlash(redirect.to, {
|
|
50
|
+
trailingSlash,
|
|
51
|
+
baseUrl: pluginContext.baseUrl,
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
32
54
|
|
|
33
55
|
validateCollectedRedirects(redirects, pluginContext);
|
|
34
56
|
return filterUnwantedRedirects(redirects, pluginContext);
|
|
35
57
|
}
|
|
36
58
|
|
|
37
|
-
// If users wants to redirect to=/abc and they enable trailingSlash=true then
|
|
38
|
-
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid
|
|
39
|
-
// path now)
|
|
40
|
-
// => we want to redirect to=/abc/ without the user having to change all its
|
|
41
|
-
// redirect plugin options
|
|
42
|
-
// It should be easy to toggle siteConfig.trailingSlash option without having to
|
|
43
|
-
// change other configs
|
|
44
|
-
function applyRedirectsTrailingSlash(
|
|
45
|
-
redirects: RedirectMetadata[],
|
|
46
|
-
params: ApplyTrailingSlashParams,
|
|
47
|
-
) {
|
|
48
|
-
return redirects.map((redirect) => ({
|
|
49
|
-
...redirect,
|
|
50
|
-
to: applyTrailingSlash(redirect.to, params),
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
59
|
function validateCollectedRedirects(
|
|
55
|
-
redirects:
|
|
60
|
+
redirects: RedirectItem[],
|
|
56
61
|
pluginContext: PluginContext,
|
|
57
62
|
) {
|
|
58
63
|
const redirectValidationErrors = redirects
|
|
@@ -89,16 +94,18 @@ Valid paths you can redirect to:
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function filterUnwantedRedirects(
|
|
92
|
-
redirects:
|
|
97
|
+
redirects: RedirectItem[],
|
|
93
98
|
pluginContext: PluginContext,
|
|
94
|
-
):
|
|
99
|
+
): RedirectItem[] {
|
|
95
100
|
// We don't want to create the same redirect twice, since that would lead to
|
|
96
101
|
// writing the same html redirection file twice.
|
|
97
102
|
Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach(
|
|
98
103
|
([from, groupedFromRedirects]) => {
|
|
99
104
|
if (groupedFromRedirects.length > 1) {
|
|
100
|
-
logger.
|
|
101
|
-
|
|
105
|
+
logger.report(
|
|
106
|
+
pluginContext.siteConfig.onDuplicateRoutes,
|
|
107
|
+
)`name=${'@docusaurus/plugin-client-redirects'}: multiple redirects are created with the same "from" pathname: path=${from}
|
|
108
|
+
It is not possible to redirect the same pathname to multiple destinations:${groupedFromRedirects.map(
|
|
102
109
|
(r) => JSON.stringify(r),
|
|
103
110
|
)}`;
|
|
104
111
|
}
|
|
@@ -106,51 +113,29 @@ It is not possible to redirect the same pathname to multiple destinations: ${gro
|
|
|
106
113
|
);
|
|
107
114
|
const collectedRedirects = _.uniqBy(redirects, (redirect) => redirect.from);
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
const {false: newRedirects = [], true: redirectsOverridingExistingPath = []} =
|
|
117
|
+
_.groupBy(collectedRedirects, (redirect) =>
|
|
118
|
+
pluginContext.relativeRoutesPaths.includes(redirect.from),
|
|
119
|
+
);
|
|
113
120
|
if (redirectsOverridingExistingPath.length > 0) {
|
|
114
|
-
logger.
|
|
121
|
+
logger.report(
|
|
122
|
+
pluginContext.siteConfig.onDuplicateRoutes,
|
|
123
|
+
)`name=${'@docusaurus/plugin-client-redirects'}: some redirects would override existing paths, and will be ignored:${redirectsOverridingExistingPath.map(
|
|
115
124
|
(r) => JSON.stringify(r),
|
|
116
125
|
)}`;
|
|
117
126
|
}
|
|
118
|
-
return
|
|
119
|
-
(redirect) => !pluginContext.relativeRoutesPaths.includes(redirect.from),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// For each plugin config option, create the appropriate redirects
|
|
124
|
-
function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] {
|
|
125
|
-
return [
|
|
126
|
-
...createFromExtensionsRedirects(
|
|
127
|
-
pluginContext.relativeRoutesPaths,
|
|
128
|
-
pluginContext.options.fromExtensions,
|
|
129
|
-
),
|
|
130
|
-
...createToExtensionsRedirects(
|
|
131
|
-
pluginContext.relativeRoutesPaths,
|
|
132
|
-
pluginContext.options.toExtensions,
|
|
133
|
-
),
|
|
134
|
-
...createRedirectsOptionRedirects(pluginContext.options.redirects),
|
|
135
|
-
...createCreateRedirectsOptionRedirects(
|
|
136
|
-
pluginContext.relativeRoutesPaths,
|
|
137
|
-
pluginContext.options.createRedirects,
|
|
138
|
-
),
|
|
139
|
-
];
|
|
127
|
+
return newRedirects;
|
|
140
128
|
}
|
|
141
129
|
|
|
142
130
|
function createRedirectsOptionRedirects(
|
|
143
131
|
redirectsOption: PluginOptions['redirects'],
|
|
144
|
-
):
|
|
132
|
+
): RedirectItem[] {
|
|
145
133
|
// For convenience, user can use a string or a string[]
|
|
146
|
-
function optionToRedirects(option: RedirectOption):
|
|
134
|
+
function optionToRedirects(option: RedirectOption): RedirectItem[] {
|
|
147
135
|
if (typeof option.from === 'string') {
|
|
148
136
|
return [{from: option.from, to: option.to}];
|
|
149
137
|
}
|
|
150
|
-
return option.from.map((from) => ({
|
|
151
|
-
from,
|
|
152
|
-
to: option.to,
|
|
153
|
-
}));
|
|
138
|
+
return option.from.map((from) => ({from, to: option.to}));
|
|
154
139
|
}
|
|
155
140
|
|
|
156
141
|
return redirectsOption.flatMap(optionToRedirects);
|
|
@@ -160,17 +145,14 @@ function createRedirectsOptionRedirects(
|
|
|
160
145
|
function createCreateRedirectsOptionRedirects(
|
|
161
146
|
paths: string[],
|
|
162
147
|
createRedirects: PluginOptions['createRedirects'],
|
|
163
|
-
):
|
|
164
|
-
function createPathRedirects(path: string):
|
|
148
|
+
): RedirectItem[] {
|
|
149
|
+
function createPathRedirects(path: string): RedirectItem[] {
|
|
165
150
|
const fromsMixed: string | string[] = createRedirects?.(path) ?? [];
|
|
166
151
|
|
|
167
152
|
const froms: string[] =
|
|
168
153
|
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
|
|
169
154
|
|
|
170
|
-
return froms.map((from) => ({
|
|
171
|
-
from,
|
|
172
|
-
to: path,
|
|
173
|
-
}));
|
|
155
|
+
return froms.map((from) => ({from, to: path}));
|
|
174
156
|
}
|
|
175
157
|
|
|
176
158
|
return paths.flatMap(createPathRedirects);
|
package/src/deps.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// TODO incompatible declaration file
|
|
9
|
+
declare module 'eta' {
|
|
10
|
+
export const defaultConfig: object;
|
|
11
|
+
|
|
12
|
+
export function compile(
|
|
13
|
+
template: string,
|
|
14
|
+
options?: object,
|
|
15
|
+
): (data: object, config: object) => string;
|
|
16
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
removeSuffix,
|
|
11
11
|
removeTrailingSlash,
|
|
12
12
|
} from '@docusaurus/utils';
|
|
13
|
-
import type {
|
|
13
|
+
import type {RedirectItem} from './types';
|
|
14
14
|
|
|
15
15
|
const ExtensionAdditionalMessage =
|
|
16
16
|
'If the redirect extension system is not good enough for your use case, you can create redirects yourself with the "createRedirects" plugin option.';
|
|
@@ -40,23 +40,21 @@ const validateExtension = (ext: string) => {
|
|
|
40
40
|
|
|
41
41
|
const addLeadingDot = (extension: string) => `.${extension}`;
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Create new `/path` that redirects to existing an `/path.html`
|
|
45
|
+
*/
|
|
44
46
|
export function createToExtensionsRedirects(
|
|
45
47
|
paths: string[],
|
|
46
48
|
extensions: string[],
|
|
47
|
-
):
|
|
49
|
+
): RedirectItem[] {
|
|
48
50
|
extensions.forEach(validateExtension);
|
|
49
51
|
|
|
50
52
|
const dottedExtensions = extensions.map(addLeadingDot);
|
|
51
53
|
|
|
52
|
-
const createPathRedirects = (path: string):
|
|
54
|
+
const createPathRedirects = (path: string): RedirectItem[] => {
|
|
53
55
|
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
|
|
54
56
|
if (extensionFound) {
|
|
55
|
-
|
|
56
|
-
return [routePathWithoutExtension].map((from) => ({
|
|
57
|
-
from,
|
|
58
|
-
to: path,
|
|
59
|
-
}));
|
|
57
|
+
return [{from: removeSuffix(path, extensionFound), to: path}];
|
|
60
58
|
}
|
|
61
59
|
return [];
|
|
62
60
|
};
|
|
@@ -64,12 +62,15 @@ export function createToExtensionsRedirects(
|
|
|
64
62
|
return paths.flatMap(createPathRedirects);
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Create new `/path.html/index.html` that redirects to existing an `/path`
|
|
67
|
+
* The filename pattern might look weird but it's on purpose (see
|
|
68
|
+
* https://github.com/facebook/docusaurus/issues/5055)
|
|
69
|
+
*/
|
|
69
70
|
export function createFromExtensionsRedirects(
|
|
70
71
|
paths: string[],
|
|
71
72
|
extensions: string[],
|
|
72
|
-
):
|
|
73
|
+
): RedirectItem[] {
|
|
73
74
|
extensions.forEach(validateExtension);
|
|
74
75
|
|
|
75
76
|
const dottedExtensions = extensions.map(addLeadingDot);
|
|
@@ -77,7 +78,7 @@ export function createFromExtensionsRedirects(
|
|
|
77
78
|
const alreadyEndsWithAnExtension = (str: string) =>
|
|
78
79
|
dottedExtensions.some((ext) => str.endsWith(ext));
|
|
79
80
|
|
|
80
|
-
const createPathRedirects = (path: string):
|
|
81
|
+
const createPathRedirects = (path: string): RedirectItem[] => {
|
|
81
82
|
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
|
|
82
83
|
return [];
|
|
83
84
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
|
9
9
|
import collectRedirects from './collectRedirects';
|
|
10
10
|
import writeRedirectFiles, {
|
|
11
|
-
|
|
12
|
-
type
|
|
11
|
+
toRedirectFiles,
|
|
12
|
+
type RedirectFile,
|
|
13
13
|
} from './writeRedirectFiles';
|
|
14
14
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
|
15
|
-
import type {PluginContext,
|
|
15
|
+
import type {PluginContext, RedirectItem} from './types';
|
|
16
16
|
import type {PluginOptions, Options} from './options';
|
|
17
17
|
|
|
18
18
|
export default function pluginClientRedirectsPages(
|
|
@@ -31,14 +31,15 @@ export default function pluginClientRedirectsPages(
|
|
|
31
31
|
baseUrl: props.baseUrl,
|
|
32
32
|
outDir: props.outDir,
|
|
33
33
|
options,
|
|
34
|
+
siteConfig: props.siteConfig,
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
const redirects:
|
|
37
|
+
const redirects: RedirectItem[] = collectRedirects(
|
|
37
38
|
pluginContext,
|
|
38
39
|
trailingSlash,
|
|
39
40
|
);
|
|
40
41
|
|
|
41
|
-
const redirectFiles:
|
|
42
|
+
const redirectFiles: RedirectFile[] = toRedirectFiles(
|
|
42
43
|
redirects,
|
|
43
44
|
pluginContext,
|
|
44
45
|
trailingSlash,
|
package/src/options.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
|
|
|
9
9
|
import type {OptionValidationContext} from '@docusaurus/types';
|
|
10
10
|
|
|
11
11
|
export type RedirectOption = {
|
|
12
|
+
/** Pathname of an existing Docusaurus page */
|
|
12
13
|
to: string;
|
|
14
|
+
/** Pathname of the new page(s) we should create */
|
|
13
15
|
from: string | string[];
|
|
14
16
|
};
|
|
15
17
|
|
|
@@ -23,7 +25,9 @@ export type PluginOptions = {
|
|
|
23
25
|
/** The list of redirect rules, each one with multiple `from`s → one `to`. */
|
|
24
26
|
redirects: RedirectOption[];
|
|
25
27
|
/**
|
|
26
|
-
* A callback to create a redirect rule.
|
|
28
|
+
* A callback to create a redirect rule. Docusaurus query this callback
|
|
29
|
+
* against every path it has created, and use its return value to output more
|
|
30
|
+
* paths.
|
|
27
31
|
* @returns All the paths from which we should redirect to `path`
|
|
28
32
|
*/
|
|
29
33
|
createRedirects?: (
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
|
|
9
|
-
import type {
|
|
9
|
+
import type {RedirectItem} from './types';
|
|
10
10
|
|
|
11
|
-
const RedirectSchema = Joi.object<
|
|
11
|
+
const RedirectSchema = Joi.object<RedirectItem>({
|
|
12
12
|
from: PathnameSchema.required(),
|
|
13
13
|
to: PathnameSchema.required(),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
export function validateRedirect(redirect:
|
|
16
|
+
export function validateRedirect(redirect: RedirectItem): void {
|
|
17
17
|
const {error} = RedirectSchema.validate(redirect, {
|
|
18
18
|
abortEarly: true,
|
|
19
19
|
convert: false,
|
package/src/types.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {PluginOptions} from './options';
|
|
|
11
11
|
/**
|
|
12
12
|
* The minimal infos the plugin needs to work
|
|
13
13
|
*/
|
|
14
|
-
export type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
|
|
14
|
+
export type PluginContext = Pick<Props, 'outDir' | 'baseUrl' | 'siteConfig'> & {
|
|
15
15
|
options: PluginOptions;
|
|
16
16
|
relativeRoutesPaths: string[];
|
|
17
17
|
};
|
|
@@ -21,7 +21,7 @@ export type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
|
|
|
21
21
|
* /!\ easy to be confused: "from" is the new page we should create,
|
|
22
22
|
* that redirects to "to": the existing Docusaurus page
|
|
23
23
|
*/
|
|
24
|
-
export type
|
|
24
|
+
export type RedirectItem = {
|
|
25
25
|
/** Pathname of the new page we should create */
|
|
26
26
|
from: string;
|
|
27
27
|
/** Pathname of an existing Docusaurus page */
|
|
@@ -13,11 +13,11 @@ import {normalizeUrl} from '@docusaurus/utils';
|
|
|
13
13
|
|
|
14
14
|
import createRedirectPageContent from './createRedirectPageContent';
|
|
15
15
|
|
|
16
|
-
import type {PluginContext,
|
|
16
|
+
import type {PluginContext, RedirectItem} from './types';
|
|
17
17
|
|
|
18
18
|
export type WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>;
|
|
19
19
|
|
|
20
|
-
export type
|
|
20
|
+
export type RedirectFile = {
|
|
21
21
|
fileAbsolutePath: string;
|
|
22
22
|
fileContent: string;
|
|
23
23
|
};
|
|
@@ -57,11 +57,11 @@ function getRedirectFilePath(
|
|
|
57
57
|
return path.join(filePath, `${fileName}/index.html`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function
|
|
61
|
-
redirects:
|
|
60
|
+
export function toRedirectFiles(
|
|
61
|
+
redirects: RedirectItem[],
|
|
62
62
|
pluginContext: WriteFilesPluginContext,
|
|
63
63
|
trailingSlash: boolean | undefined,
|
|
64
|
-
):
|
|
64
|
+
): RedirectFile[] {
|
|
65
65
|
// Perf: avoid rendering the template twice with the exact same "props"
|
|
66
66
|
// We might create multiple redirect pages for the same destination url
|
|
67
67
|
// note: the first fn arg is the cache key!
|
|
@@ -69,7 +69,7 @@ export function toRedirectFilesMetadata(
|
|
|
69
69
|
createRedirectPageContent({toUrl}),
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
-
const createFileMetadata = (redirect:
|
|
72
|
+
const createFileMetadata = (redirect: RedirectItem) => {
|
|
73
73
|
const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash);
|
|
74
74
|
const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath);
|
|
75
75
|
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
|
|
@@ -84,9 +84,7 @@ export function toRedirectFilesMetadata(
|
|
|
84
84
|
return redirects.map(createFileMetadata);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export async function writeRedirectFile(
|
|
88
|
-
file: RedirectFileMetadata,
|
|
89
|
-
): Promise<void> {
|
|
87
|
+
export async function writeRedirectFile(file: RedirectFile): Promise<void> {
|
|
90
88
|
try {
|
|
91
89
|
// User-friendly security to prevent file overrides
|
|
92
90
|
if (await fs.pathExists(file.fileAbsolutePath)) {
|
|
@@ -108,7 +106,7 @@ export async function writeRedirectFile(
|
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
export default async function writeRedirectFiles(
|
|
111
|
-
redirectFiles:
|
|
109
|
+
redirectFiles: RedirectFile[],
|
|
112
110
|
): Promise<void> {
|
|
113
111
|
await Promise.all(redirectFiles.map(writeRedirectFile));
|
|
114
112
|
}
|