From fd47ca1925b8aeacab257dabe3b6b650c33105bc Mon Sep 17 00:00:00 2001 From: slorber Date: Fri, 22 May 2020 20:15:16 +0200 Subject: [PATCH] feat(v2): docusaurus-plugin-client-redirects --- .eslintignore | 1 + .gitignore | 1 + .../package.json | 26 ++++ .../src/createRedirectPageContent.ts | 35 +++++ .../src/index.ts | 144 ++++++++++++++++++ .../src/redirectCreators.ts | 41 +++++ .../src/types.ts | 18 +++ .../src/utils.ts | 23 +++ .../tsconfig.json | 9 ++ website/docusaurus.config.js | 1 + website/package.json | 1 + 11 files changed, 300 insertions(+) create mode 100644 packages/docusaurus-plugin-client-redirects/package.json create mode 100644 packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/index.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/types.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/utils.ts create mode 100644 packages/docusaurus-plugin-client-redirects/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 1dc5c56918..c56f7af44c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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/ diff --git a/.gitignore b/.gitignore index 3462819d8b..418b6c11d1 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/packages/docusaurus-plugin-client-redirects/package.json b/packages/docusaurus-plugin-client-redirects/package.json new file mode 100644 index 0000000000..a3041d5e6b --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/package.json @@ -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" + } +} diff --git a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts new file mode 100644 index 0000000000..140a098569 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts @@ -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 ` + + + + + + + +`; +} diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts new file mode 100644 index 0000000000..27a0927227 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -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, +): Plugin { + 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)); +} diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts new file mode 100644 index 0000000000..a889f776f6 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts @@ -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}`); + } + }; +} diff --git a/packages/docusaurus-plugin-client-redirects/src/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts new file mode 100644 index 0000000000..9e8a7fe024 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/types.ts @@ -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; diff --git a/packages/docusaurus-plugin-client-redirects/src/utils.ts b/packages/docusaurus-plugin-client-redirects/src/utils.ts new file mode 100644 index 0000000000..33e954e902 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/utils.ts @@ -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`); +} diff --git a/packages/docusaurus-plugin-client-redirects/tsconfig.json b/packages/docusaurus-plugin-client-redirects/tsconfig.json new file mode 100644 index 0000000000..f5902ba108 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "rootDir": "src", + "outDir": "lib" + } +} diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ff1d8b53a6..ca973602b7 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -21,6 +21,7 @@ module.exports = { }, themes: ['@docusaurus/theme-live-codeblock'], plugins: [ + ['@docusaurus/plugin-client-redirects', {}], [ '@docusaurus/plugin-ideal-image', { diff --git a/website/package.json b/website/package.json index 885ff449e3..bb43e441c7 100644 --- a/website/package.json +++ b/website/package.json @@ -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",