feat(v2): docusaurus-plugin-client-redirects

This commit is contained in:
slorber 2020-05-22 20:15:16 +02:00
parent 05c3aa31f4
commit fd47ca1925
11 changed files with 300 additions and 0 deletions

View file

@ -13,6 +13,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
View file

@ -17,6 +17,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/

View file

@ -0,0 +1,26 @@
{
"name": "@docusaurus/plugin-client-redirects",
"version": "2.0.0-alpha.55",
"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.55",
"@docusaurus/utils": "^2.0.0-alpha.55",
"globby": "^10.0.1"
},
"peerDependencies": {
"@docusaurus/core": "^2.0.0",
"react": "^16.8.4",
"react-dom": "^16.8.4"
},
"engines": {
"node": ">=10.9.0"
}
}

View file

@ -0,0 +1,35 @@
/**
* 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.
*/
type CreateRedirectPageOptions = {
toUrl: string;
};
export default function createRedirectPageContent({
toUrl,
}: CreateRedirectPageOptions) {
return `
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=${toUrl}">
<script>
const redirectLink = '${toUrl}' + location.search + location.hash;
document.write('<link rel="canonical" href="' + redirectLink + '">');
document.write('<title>Redirecting to ' + redirectLink + '</title>');
document.write('</head>')
document.write('<body>')
document.write('If you are not redirected automatically, follow this <a href="' + redirectLink + '">link</a>.')
document.write('</body>')
setTimeout(() => {
window.location.assign(redirectLink)
})
</script>
</html>
`;
}

View file

@ -0,0 +1,144 @@
/**
* 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 {flatten} from 'lodash';
import {LoadContext, Plugin, Props} from '@docusaurus/types';
import {PluginOptions, RedirectsCreator} from './types';
import createRedirectPageContent from './createRedirectPageContent';
import {addTrailingSlash, getFilePathForRoutePath} from './utils';
import {
fromExtensionsRedirectCreator,
toExtensionsRedirectCreator,
} from './redirectCreators';
const DEFAULT_OPTIONS: PluginOptions = {
fromExtensions: [],
toExtensions: [],
};
type RedirectMetadata = {
fromRoutePath: string;
toRoutePath: string;
toUrl: string;
redirectPageContent: string;
redirectAbsoluteFilePath: string;
};
type PluginContext = {
props: Props;
options: PluginOptions;
redirectsCreators: RedirectsCreator[];
};
export default function pluginClientRedirectsPages(
_context: LoadContext,
opts: Partial<PluginOptions>,
): Plugin<unknown> {
const options = {...DEFAULT_OPTIONS, ...opts};
return {
name: 'docusaurus-plugin-client-redirects',
async postBuild(props: Props) {
const redirectsCreators: RedirectsCreator[] = buildRedirectCreators(
options,
);
const pluginContext: PluginContext = {props, options, redirectsCreators};
// Process in 2 steps, to make code more easy to test
const redirects: RedirectMetadata[] = collectRoutePathRedirects(
pluginContext,
);
console.log('redirects=', redirects);
await writeRedirectFiles(redirects);
},
};
}
function buildRedirectCreators(options: PluginOptions): RedirectsCreator[] {
const noopRedirectCreator: RedirectsCreator = (_routePath: string) => [];
return [
fromExtensionsRedirectCreator(options.fromExtensions),
toExtensionsRedirectCreator(options.toExtensions),
options.createRedirects ?? noopRedirectCreator,
];
}
function collectRoutePathRedirects(
pluginContext: PluginContext,
): RedirectMetadata[] {
return flatten(
pluginContext.redirectsCreators.map((redirectCreator) => {
return createRoutesPathsRedirects(redirectCreator, pluginContext);
}),
);
}
// Create all redirects for a list of route path
function createRoutesPathsRedirects(
redirectCreator: RedirectsCreator,
pluginContext: PluginContext,
): RedirectMetadata[] {
return flatten(
pluginContext.props.routesPaths.map((routePath) =>
createRoutePathRedirects(routePath, redirectCreator, pluginContext),
),
);
}
// Create all redirects for a single route path
function createRoutePathRedirects(
routePath: string,
redirectCreator: RedirectsCreator,
{props}: PluginContext,
): RedirectMetadata[] {
const {siteConfig, outDir} = props;
// TODO do we receive absolute urls???
if (!path.isAbsolute(routePath)) {
return [];
}
// TODO addTrailingSlash ?
const toUrl = addTrailingSlash(`${siteConfig.url}${routePath}`);
const redirectPageContent = createRedirectPageContent({toUrl});
const fromRoutePaths: string[] = redirectCreator(routePath) ?? [];
return fromRoutePaths.map((fromRoutePath) => {
const redirectAbsoluteFilePath = path.join(
outDir,
getFilePathForRoutePath(fromRoutePath),
);
return {
fromRoutePath,
toRoutePath: routePath,
toUrl,
redirectPageContent,
redirectAbsoluteFilePath,
};
});
}
async function writeRedirectFiles(redirects: RedirectMetadata[]) {
async function writeRedirectFile(redirect: RedirectMetadata) {
try {
await fs.writeFile(
redirect.redirectAbsoluteFilePath,
redirect.redirectPageContent,
);
} catch (err) {
throw new Error(`Redirect file creation error: ${err}`);
}
}
await Promise.all(redirects.map(writeRedirectFile));
}

View file

@ -0,0 +1,41 @@
/**
* 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 {RedirectsCreator} from './types';
import {removeTrailingSlash} from './utils';
export function fromExtensionsRedirectCreator(
extensions: string[],
): RedirectsCreator {
const dottedExtensions = extensions.map((ext) => `.${ext}`);
return (fromRoutePath: string) => {
const extensionMatch = dottedExtensions.find((ext) =>
fromRoutePath.endsWith(`.${ext}`),
);
if (extensionMatch) {
const routePathWithoutExtension = fromRoutePath.substr(
0,
fromRoutePath.length - extensionMatch.length - 1,
);
return [routePathWithoutExtension];
}
return [];
};
}
export function toExtensionsRedirectCreator(
extensions: string[],
): RedirectsCreator {
return (fromRoutePath: string) => {
if (fromRoutePath === '/') {
return [];
} else {
const fromRoutePathNoSlash = removeTrailingSlash(fromRoutePath);
return extensions.map((ext) => `${fromRoutePathNoSlash}.${ext}`);
}
};
}

View file

@ -0,0 +1,18 @@
/**
* 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 type PluginOptions = {
fromExtensions: string[];
toExtensions: string[];
createRedirects?: RedirectsCreator;
};
// For a given existing route path,
// return all the paths from which we should redirect from
export type RedirectsCreator = (
routePath: string,
) => string[] | null | undefined;

View file

@ -0,0 +1,23 @@
/**
* 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 path from 'path';
export function addTrailingSlash(str: string) {
return str.endsWith('/') ? '' : '/';
}
export function removeTrailingSlash(str: string) {
return str.endsWith('/') ? str.substr(0, str.length - 2) : str;
}
// TODO does this function already exist?
export function getFilePathForRoutePath(routePath: string) {
const fileName = path.basename(routePath);
const filePath = path.dirname(routePath);
return path.join(filePath, `${fileName}.html`);
}

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"outDir": "lib"
}
}

View file

@ -21,6 +21,7 @@ module.exports = {
},
themes: ['@docusaurus/theme-live-codeblock'],
plugins: [
['@docusaurus/plugin-client-redirects', {}],
[
'@docusaurus/plugin-ideal-image',
{

View file

@ -11,6 +11,7 @@
"dependencies": {
"@docusaurus/core": "^2.0.0-alpha.55",
"@docusaurus/plugin-ideal-image": "^2.0.0-alpha.55",
"@docusaurus/plugin-client-redirects": "^2.0.0-alpha.55",
"@docusaurus/preset-classic": "^2.0.0-alpha.55",
"classnames": "^2.2.6",
"color": "^3.1.2",