From fd47ca1925b8aeacab257dabe3b6b650c33105bc Mon Sep 17 00:00:00 2001 From: slorber Date: Fri, 22 May 2020 20:15:16 +0200 Subject: [PATCH 01/41] 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", From fde4933cd5100f858a0395380272ed7d0e825494 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 16:03:39 +0200 Subject: [PATCH 02/41] add utils tests --- .../src/__tests__/utils.test.ts | 46 +++++++++++++++++++ .../src/utils.ts | 6 +-- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..a2c4cf07c3 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts @@ -0,0 +1,46 @@ +/** + * 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 { + addTrailingSlash, + removeTrailingSlash, + getFilePathForRoutePath, +} from '../utils'; + +describe('addTrailingSlash', () => { + test('should noop', () => { + expect(addTrailingSlash('/abcd/')).toEqual('/abcd/'); + }); + test('should add /', () => { + expect(addTrailingSlash('/abcd')).toEqual('/abcd/'); + }); +}); + +describe('removeTrailingSlash', () => { + test('should noop', () => { + expect(removeTrailingSlash('/abcd')).toEqual('/abcd'); + }); + test('should remove /', () => { + expect(removeTrailingSlash('/abcd/')).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', + ); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/utils.ts b/packages/docusaurus-plugin-client-redirects/src/utils.ts index 33e954e902..b5ed63d764 100644 --- a/packages/docusaurus-plugin-client-redirects/src/utils.ts +++ b/packages/docusaurus-plugin-client-redirects/src/utils.ts @@ -8,16 +8,16 @@ import path from 'path'; export function addTrailingSlash(str: string) { - return str.endsWith('/') ? '' : '/'; + return str.endsWith('/') ? str : `${str}/`; } export function removeTrailingSlash(str: string) { - return str.endsWith('/') ? str.substr(0, str.length - 2) : str; + return str.endsWith('/') ? str.slice(0, -1) : 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`); + return path.join(filePath, `${fileName}/index.html`); } From d1a162ec30ad1ef61d785d2ccc54104d279defc6 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 16:27:36 +0200 Subject: [PATCH 03/41] createRedirectPageContent: use eta template + encode URI for meta refresh header + tests --- .../package.json | 1 + .../createRedirectPageContent.test.ts.snap | 27 +++++++++++++++++++ .../createRedirectPageContent.test.ts | 26 ++++++++++++++++++ .../src/createRedirectPageContent.ts | 26 +++++------------- .../template/redirectPage.html.template.js | 19 +++++++++++++ 5 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/createRedirectPageContent.test.ts.snap create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/createRedirectPageContent.test.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js diff --git a/packages/docusaurus-plugin-client-redirects/package.json b/packages/docusaurus-plugin-client-redirects/package.json index a3041d5e6b..54a40a92ca 100644 --- a/packages/docusaurus-plugin-client-redirects/package.json +++ b/packages/docusaurus-plugin-client-redirects/package.json @@ -13,6 +13,7 @@ "dependencies": { "@docusaurus/types": "^2.0.0-alpha.55", "@docusaurus/utils": "^2.0.0-alpha.55", + "eta": "^1.1.1", "globby": "^10.0.1" }, "peerDependencies": { diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/createRedirectPageContent.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/createRedirectPageContent.test.ts.snap new file mode 100644 index 0000000000..5a011ab162 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/createRedirectPageContent.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createRedirectPageContent should encode uri special chars 1`] = ` +" + + + + + + +" +`; + +exports[`createRedirectPageContent should match snapshot 1`] = ` +" + + + + + + +" +`; diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/createRedirectPageContent.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/createRedirectPageContent.test.ts new file mode 100644 index 0000000000..b9aea1ef34 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/createRedirectPageContent.test.ts @@ -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(); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts index 140a098569..d7b9452328 100644 --- a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts +++ b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts @@ -5,6 +5,9 @@ * LICENSE file in the root directory of this source tree. */ +const eta = require('eta'); +const redirectPageTemplate = require('./template/redirectPage.html.template'); + type CreateRedirectPageOptions = { toUrl: string; }; @@ -12,24 +15,7 @@ type CreateRedirectPageOptions = { export default function createRedirectPageContent({ toUrl, }: CreateRedirectPageOptions) { - return ` - - - - - - - -`; + return eta.render(redirectPageTemplate.trim(), { + toUrl: encodeURI(toUrl), + }); } diff --git a/packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js b/packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js new file mode 100644 index 0000000000..e36d306bb8 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js @@ -0,0 +1,19 @@ +/** + * 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. + */ + +module.exports = ` + + + + + + + + +`; From 92c2d0839c40eb724eb440d607bd69645c550a04 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 17:07:00 +0200 Subject: [PATCH 04/41] add tests for redirectCreators --- .../src/__tests__/redirectCreators.test.ts | 87 +++++++++++++++++++ .../src/__tests__/utils.test.ts | 20 ++++- .../src/redirectCreators.ts | 65 +++++++++++--- .../src/utils.ts | 9 +- 4 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts new file mode 100644 index 0000000000..435b832e5f --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts @@ -0,0 +1,87 @@ +/** + * 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 { + fromExtensionsRedirectCreator, + toExtensionsRedirectCreator, +} from '../redirectCreators'; +import {RedirectsCreator} from '../types'; + +const createExtensionValidationTests = ( + redirectCreatorFactory: (extensions: string[]) => RedirectsCreator, +) => { + test('should reject empty extensions', () => { + expect(() => { + redirectCreatorFactory(['.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(() => { + redirectCreatorFactory(['.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(() => { + redirectCreatorFactory(['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(() => { + redirectCreatorFactory([',']); + }).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('fromExtensionsRedirectCreator', () => { + createExtensionValidationTests(fromExtensionsRedirectCreator); + + test('should create redirects from html/htm extensions', () => { + const redirectCreator = fromExtensionsRedirectCreator(['html', 'htm']); + expect(redirectCreator('')).toEqual([]); + expect(redirectCreator('/')).toEqual([]); + expect(redirectCreator('/abc.html')).toEqual(['/abc']); + expect(redirectCreator('/abc.htm')).toEqual(['/abc']); + expect(redirectCreator('/abc.xyz')).toEqual([]); + }); + + test('should not create redirection for an empty extension array', () => { + const redirectCreator = fromExtensionsRedirectCreator([]); + expect(redirectCreator('')).toEqual([]); + expect(redirectCreator('/')).toEqual([]); + expect(redirectCreator('/abc.html')).toEqual([]); + }); +}); + +describe('toExtensionsRedirectCreator', () => { + createExtensionValidationTests(toExtensionsRedirectCreator); + + test('should create redirects to html/htm extensions', () => { + const redirectCreator = toExtensionsRedirectCreator(['html', 'htm']); + expect(redirectCreator('')).toEqual([]); + expect(redirectCreator('/')).toEqual([]); + expect(redirectCreator('/abc')).toEqual(['/abc.html', '/abc.htm']); + expect(redirectCreator('/def.html')).toEqual([]); + expect(redirectCreator('/def/')).toEqual([]); + }); + + test('should not create redirection for an empty extension array', () => { + const redirectCreator = toExtensionsRedirectCreator([]); + expect(redirectCreator('')).toEqual([]); + expect(redirectCreator('/')).toEqual([]); + expect(redirectCreator('/abc')).toEqual([]); + expect(redirectCreator('/def.html')).toEqual([]); + expect(redirectCreator('/def/')).toEqual([]); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts index a2c4cf07c3..8e7cc6a19d 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/utils.test.ts @@ -8,11 +8,12 @@ import { addTrailingSlash, removeTrailingSlash, + removeSuffix, getFilePathForRoutePath, } from '../utils'; describe('addTrailingSlash', () => { - test('should noop', () => { + test('should no-op', () => { expect(addTrailingSlash('/abcd/')).toEqual('/abcd/'); }); test('should add /', () => { @@ -21,7 +22,7 @@ describe('addTrailingSlash', () => { }); describe('removeTrailingSlash', () => { - test('should noop', () => { + test('should no-op', () => { expect(removeTrailingSlash('/abcd')).toEqual('/abcd'); }); test('should remove /', () => { @@ -29,6 +30,21 @@ describe('removeTrailingSlash', () => { }); }); +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'); diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts index a889f776f6..6a9256b2d3 100644 --- a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts +++ b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts @@ -6,20 +6,53 @@ */ import {RedirectsCreator} from './types'; -import {removeTrailingSlash} from './utils'; +import {removeSuffix} from './utils'; + +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}`; export function fromExtensionsRedirectCreator( extensions: string[], ): RedirectsCreator { - const dottedExtensions = extensions.map((ext) => `.${ext}`); + extensions.forEach(validateExtension); + + const dottedExtensions = extensions.map(addLeadingDot); + return (fromRoutePath: string) => { - const extensionMatch = dottedExtensions.find((ext) => - fromRoutePath.endsWith(`.${ext}`), + const extensionFound = dottedExtensions.find((ext) => + fromRoutePath.endsWith(ext), ); - if (extensionMatch) { - const routePathWithoutExtension = fromRoutePath.substr( - 0, - fromRoutePath.length - extensionMatch.length - 1, + if (extensionFound) { + const routePathWithoutExtension = removeSuffix( + fromRoutePath, + extensionFound, ); return [routePathWithoutExtension]; } @@ -30,12 +63,22 @@ export function fromExtensionsRedirectCreator( export function toExtensionsRedirectCreator( extensions: string[], ): RedirectsCreator { + extensions.forEach(validateExtension); + + const dottedExtensions = extensions.map(addLeadingDot); + + const alreadyEndsWithAnExtension = (str: string) => + dottedExtensions.some((ext) => str.endsWith(ext)); + return (fromRoutePath: string) => { - if (fromRoutePath === '/') { + if ( + fromRoutePath === '' || + fromRoutePath.endsWith('/') || + alreadyEndsWithAnExtension(fromRoutePath) + ) { return []; } else { - const fromRoutePathNoSlash = removeTrailingSlash(fromRoutePath); - return extensions.map((ext) => `${fromRoutePathNoSlash}.${ext}`); + return extensions.map((ext) => `${fromRoutePath}.${ext}`); } }; } diff --git a/packages/docusaurus-plugin-client-redirects/src/utils.ts b/packages/docusaurus-plugin-client-redirects/src/utils.ts index b5ed63d764..8c082c25e3 100644 --- a/packages/docusaurus-plugin-client-redirects/src/utils.ts +++ b/packages/docusaurus-plugin-client-redirects/src/utils.ts @@ -12,7 +12,14 @@ export function addTrailingSlash(str: string) { } export function removeTrailingSlash(str: string) { - return str.endsWith('/') ? str.slice(0, -1) : str; + 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; } // TODO does this function already exist? From 2695548b93556ea1a8539f124cfcb7445f44062b Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 18:05:57 +0200 Subject: [PATCH 05/41] add proper normalizePluginOptions + tests --- .../package.json | 6 +- .../__tests__/normalizePluginOptions.test.ts | 78 +++++++++++++++++++ .../src/index.ts | 13 ++-- .../src/normalizePluginOptions.ts | 45 +++++++++++ .../src/types.ts | 2 + yarn.lock | 50 ++++++++++++ 6 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts diff --git a/packages/docusaurus-plugin-client-redirects/package.json b/packages/docusaurus-plugin-client-redirects/package.json index 54a40a92ca..243608459b 100644 --- a/packages/docusaurus-plugin-client-redirects/package.json +++ b/packages/docusaurus-plugin-client-redirects/package.json @@ -14,7 +14,8 @@ "@docusaurus/types": "^2.0.0-alpha.55", "@docusaurus/utils": "^2.0.0-alpha.55", "eta": "^1.1.1", - "globby": "^10.0.1" + "globby": "^10.0.1", + "yup": "^0.29.0" }, "peerDependencies": { "@docusaurus/core": "^2.0.0", @@ -23,5 +24,8 @@ }, "engines": { "node": ">=10.9.0" + }, + "devDependencies": { + "@types/yup": "^0.29.0" } } diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts new file mode 100644 index 0000000000..6626b4a7e2 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts @@ -0,0 +1,78 @@ +/** + * 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 {RedirectsCreator} 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: RedirectsCreator = (_routePath: string) => { + return []; + }; + expect( + normalizePluginOptions({ + fromExtensions: ['exe', 'zip'], + toExtensions: ['html'], + createRedirects, + }), + ).toEqual({ + fromExtensions: ['exe', 'zip'], + toExtensions: ['html'], + createRedirects, + }); + }); + + test('should reject bad fromExtensions user inputs', () => { + expect(() => + normalizePluginOptions({ + fromExtensions: [null, undefined, 123, true] as any, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "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()\`" + `); + }); + + test('should reject bad toExtensions user inputs', () => { + expect(() => + normalizePluginOptions({ + toExtensions: [null, undefined, 123, true] as any, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "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()\`" + `); + }); + + test('should reject bad createRedirects user inputs', () => { + expect(() => + normalizePluginOptions({ + createRedirects: ['bad', 'value'] as any, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"createRedirects should be a function"`, + ); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index 27a0927227..59037874fc 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -11,19 +11,15 @@ import {flatten} from 'lodash'; import {LoadContext, Plugin, Props} from '@docusaurus/types'; -import {PluginOptions, RedirectsCreator} from './types'; +import {PluginOptions, UserPluginOptions, RedirectsCreator} from './types'; import createRedirectPageContent from './createRedirectPageContent'; +import normalizePluginOptions from './normalizePluginOptions'; import {addTrailingSlash, getFilePathForRoutePath} from './utils'; import { fromExtensionsRedirectCreator, toExtensionsRedirectCreator, } from './redirectCreators'; -const DEFAULT_OPTIONS: PluginOptions = { - fromExtensions: [], - toExtensions: [], -}; - type RedirectMetadata = { fromRoutePath: string; toRoutePath: string; @@ -40,9 +36,9 @@ type PluginContext = { export default function pluginClientRedirectsPages( _context: LoadContext, - opts: Partial, + opts: UserPluginOptions, ): Plugin { - const options = {...DEFAULT_OPTIONS, ...opts}; + const options = normalizePluginOptions(opts); return { name: 'docusaurus-plugin-client-redirects', async postBuild(props: Props) { @@ -51,6 +47,7 @@ export default function pluginClientRedirectsPages( ); const pluginContext: PluginContext = {props, options, redirectsCreators}; + // Process in 2 steps, to make code more easy to test const redirects: RedirectMetadata[] = collectRoutePathRedirects( pluginContext, diff --git a/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts b/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts new file mode 100644 index 0000000000..a9ae565959 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts @@ -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 {PluginOptions, RedirectsCreator, UserPluginOptions} from './types'; +import * as Yup from 'yup'; + +export const DefaultPluginOptions: PluginOptions = { + fromExtensions: [], + toExtensions: [], +}; + +function isRedirectsCreator(value: any): value is RedirectsCreator | undefined { + if (value === null || typeof value === 'undefined') { + return true; + } + return value instanceof Function; +} + +const UserOptionsSchema = Yup.object().shape({ + fromExtensions: Yup.array().of(Yup.string().required().min(0)), + toExtensions: Yup.array().of(Yup.string().required().min(0)), + createRedirects: Yup.mixed().test( + 'createRedirects', + 'createRedirects should be a function', + isRedirectsCreator, + ), +}); + +function validateUserOptions(userOptions: UserPluginOptions) { + UserOptionsSchema.validateSync(userOptions, { + abortEarly: true, + strict: true, + }); +} + +export default function normalizePluginOptions( + userPluginOptions: UserPluginOptions = {}, +): PluginOptions { + validateUserOptions(userPluginOptions); + return {...DefaultPluginOptions, ...userPluginOptions}; +} diff --git a/packages/docusaurus-plugin-client-redirects/src/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts index 9e8a7fe024..b993558ef9 100644 --- a/packages/docusaurus-plugin-client-redirects/src/types.ts +++ b/packages/docusaurus-plugin-client-redirects/src/types.ts @@ -11,6 +11,8 @@ export type PluginOptions = { createRedirects?: RedirectsCreator; }; +export type UserPluginOptions = Partial; + // For a given existing route path, // return all the paths from which we should redirect from export type RedirectsCreator = ( diff --git a/yarn.lock b/yarn.lock index e9d21b70b2..8d13f9cfa2 100644 --- a/yarn.lock +++ b/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" @@ -7657,6 +7669,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" @@ -10719,6 +10736,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" @@ -13928,6 +13950,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" @@ -16475,6 +16502,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" @@ -16837,6 +16869,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" @@ -18164,6 +18201,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" From 9579ac10fca5c5dfd72de84e1c5fb668ba157da2 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 18:21:41 +0200 Subject: [PATCH 06/41] split collectRedirects and writeRedirectFiles in separate files --- .../src/collectRedirects.ts | 89 +++++++++++++ .../src/index.ts | 126 ++---------------- .../src/types.ts | 18 +++ .../src/writeRedirectFiles.ts | 28 ++++ 4 files changed, 146 insertions(+), 115 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts create mode 100644 packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts new file mode 100644 index 0000000000..2b862e3564 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -0,0 +1,89 @@ +/** + * 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'; +import {flatten} from 'lodash'; +import { + RedirectsCreator, + PluginContext, + RedirectMetadata, + PluginOptions, +} from './types'; +import createRedirectPageContent from './createRedirectPageContent'; +import {addTrailingSlash, getFilePathForRoutePath} from './utils'; +import { + fromExtensionsRedirectCreator, + toExtensionsRedirectCreator, +} from './redirectCreators'; + +export default function collectRedirects( + pluginContext: PluginContext, +): RedirectMetadata[] { + const redirectsCreators: RedirectsCreator[] = buildRedirectCreators( + pluginContext.options, + ); + + return flatten( + redirectsCreators.map((redirectCreator) => { + return createRoutesPathsRedirects(redirectCreator, pluginContext); + }), + ); +} + +function buildRedirectCreators(options: PluginOptions): RedirectsCreator[] { + const noopRedirectCreator: RedirectsCreator = (_routePath: string) => []; + return [ + fromExtensionsRedirectCreator(options.fromExtensions), + toExtensionsRedirectCreator(options.toExtensions), + options.createRedirects ?? noopRedirectCreator, + ]; +} + +// Create all redirects for a list of route path +function createRoutesPathsRedirects( + redirectCreator: RedirectsCreator, + pluginContext: PluginContext, +): RedirectMetadata[] { + return flatten( + pluginContext.routesPaths.map((routePath) => + createRoutePathRedirects(routePath, redirectCreator, pluginContext), + ), + ); +} + +// Create all redirects for a single route path +function createRoutePathRedirects( + routePath: string, + redirectCreator: RedirectsCreator, + {siteConfig, outDir}: PluginContext, +): RedirectMetadata[] { + // 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, + }; + }); +} diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index 59037874fc..2016fbe397 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -5,137 +5,33 @@ * 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 {UserPluginOptions, PluginContext, RedirectMetadata} from './types'; -import {PluginOptions, UserPluginOptions, RedirectsCreator} from './types'; -import createRedirectPageContent from './createRedirectPageContent'; import normalizePluginOptions from './normalizePluginOptions'; -import {addTrailingSlash, getFilePathForRoutePath} from './utils'; -import { - fromExtensionsRedirectCreator, - toExtensionsRedirectCreator, -} from './redirectCreators'; - -type RedirectMetadata = { - fromRoutePath: string; - toRoutePath: string; - toUrl: string; - redirectPageContent: string; - redirectAbsoluteFilePath: string; -}; - -type PluginContext = { - props: Props; - options: PluginOptions; - redirectsCreators: RedirectsCreator[]; -}; +import collectRedirects from './collectRedirects'; +import writeRedirectFiles from './writeRedirectFiles'; export default function pluginClientRedirectsPages( _context: LoadContext, opts: UserPluginOptions, ): Plugin { const options = normalizePluginOptions(opts); + return { name: 'docusaurus-plugin-client-redirects', async postBuild(props: Props) { - const redirectsCreators: RedirectsCreator[] = buildRedirectCreators( + const pluginContext: PluginContext = { + routesPaths: props.routesPaths, + siteConfig: props.siteConfig, + outDir: props.outDir, 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); + const redirects: RedirectMetadata[] = collectRedirects(pluginContext); + // Write files only at the end: make code more easy to test without IO 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/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts index b993558ef9..b5b542484a 100644 --- a/packages/docusaurus-plugin-client-redirects/src/types.ts +++ b/packages/docusaurus-plugin-client-redirects/src/types.ts @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {Props} from '@docusaurus/types'; + export type PluginOptions = { fromExtensions: string[]; toExtensions: string[]; @@ -18,3 +20,19 @@ export type UserPluginOptions = Partial; export type RedirectsCreator = ( routePath: string, ) => string[] | null | undefined; + +// Having an in-memory representation of wanted redirects is easier to test +export type RedirectMetadata = { + fromRoutePath: string; + toRoutePath: string; + toUrl: string; + redirectPageContent: string; + redirectAbsoluteFilePath: string; +}; + +export type PluginContext = Pick< + Props, + 'routesPaths' | 'siteConfig' | 'outDir' +> & { + options: PluginOptions; +}; diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts new file mode 100644 index 0000000000..7dbda45323 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -0,0 +1,28 @@ +/** + * 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 {RedirectMetadata} from './types'; + +type RedirectFile = Pick< + RedirectMetadata, + 'redirectAbsoluteFilePath' | 'redirectPageContent' +>; + +export default async function writeRedirectFiles(redirects: RedirectFile[]) { + async function writeRedirectFile(redirect: RedirectFile) { + try { + await fs.writeFile( + redirect.redirectAbsoluteFilePath, + redirect.redirectPageContent, + ); + } catch (err) { + throw new Error(`Redirect file creation error: ${err}`); + } + } + await Promise.all(redirects.map(writeRedirectFile)); +} From d8c163e76bbc7083b79d4c442630b67d7280fe77 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 19:18:36 +0200 Subject: [PATCH 07/41] stable collectRedirects --- .../src/collectRedirects.ts | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 2b862e3564..67e27aabd9 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {flatten} from 'lodash'; +import {flatten, uniqBy} from 'lodash'; import { RedirectsCreator, PluginContext, @@ -23,6 +23,26 @@ import { export default function collectRedirects( pluginContext: PluginContext, ): RedirectMetadata[] { + const redirects = doCollectRedirects(pluginContext); + return filterUnwantedRedirects(redirects, pluginContext); +} + +function filterUnwantedRedirects( + redirects: RedirectMetadata[], + pluginContext: PluginContext, +): RedirectMetadata[] { + // we don't want to create twice the same redirect + redirects = uniqBy(redirects, (redirect) => redirect.fromRoutePath); + + // We don't want to override an existing route + redirects = redirects.filter( + (redirect) => !pluginContext.routesPaths.includes(redirect.fromRoutePath), + ); + + return redirects; +} + +function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] { const redirectsCreators: RedirectsCreator[] = buildRedirectCreators( pluginContext.options, ); @@ -50,7 +70,7 @@ function createRoutesPathsRedirects( ): RedirectMetadata[] { return flatten( pluginContext.routesPaths.map((routePath) => - createRoutePathRedirects(routePath, redirectCreator, pluginContext), + createRoutePathRedirects(routePath, redirectCreator), ), ); } @@ -59,20 +79,32 @@ function createRoutesPathsRedirects( function createRoutePathRedirects( routePath: string, redirectCreator: RedirectsCreator, - {siteConfig, outDir}: PluginContext, ): RedirectMetadata[] { + /* // TODO do we receive absolute urls??? if (!path.isAbsolute(routePath)) { return []; } + */ + + /* // TODO addTrailingSlash ? - const toUrl = addTrailingSlash(`${siteConfig.url}${routePath}`); + const toUrl = addTrailingSlash(`${baseUrl}${routePath}`); const redirectPageContent = createRedirectPageContent({toUrl}); + */ + const fromRoutePaths: string[] = redirectCreator(routePath) ?? []; + return fromRoutePaths.map((fromRoutePath) => { + return { + fromRoutePath, + toRoutePath: routePath, + }; + }); + /* return fromRoutePaths.map((fromRoutePath) => { const redirectAbsoluteFilePath = path.join( outDir, @@ -86,4 +118,6 @@ function createRoutePathRedirects( redirectAbsoluteFilePath, }; }); + + */ } From 26beb00476f4f126a88f527f43b2a6bc81bd5980 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 19:26:36 +0200 Subject: [PATCH 08/41] improve a bit collectRedirects --- .../src/__tests__/collectRedirects.test.ts | 154 ++++++++++++++++++ .../src/__tests__/redirectCreators.test.ts | 16 +- .../src/collectRedirects.ts | 2 + .../src/index.ts | 2 +- .../src/redirectCreators.ts | 6 +- .../src/types.ts | 22 +-- 6 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/collectRedirects.test.ts diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/collectRedirects.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/collectRedirects.test.ts new file mode 100644 index 0000000000..38cec1380d --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/collectRedirects.test.ts @@ -0,0 +1,154 @@ +/** + * 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 '../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([ + { + fromRoutePath: '/somePath.html', + toRoutePath: '/somePath', + }, + { + fromRoutePath: '/somePath.exe', + toRoutePath: '/somePath', + }, + ]); + }); + + test('should collect redirects to html/exe extension', () => { + expect( + collectRedirects( + createTestPluginContext( + { + toExtensions: ['html', 'exe'], + }, + ['/', '/somePath', '/otherPath.html'], + ), + ), + ).toEqual([ + { + fromRoutePath: '/otherPath', + toRoutePath: '/otherPath.html', + }, + ]); + }); + + 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([ + { + fromRoutePath: '/some/path/suffix1', + toRoutePath: '/', + }, + { + fromRoutePath: '/some/other/path/suffix2', + toRoutePath: '/', + }, + + { + fromRoutePath: '/testpath/some/path/suffix1', + toRoutePath: '/testpath', + }, + { + fromRoutePath: '/testpath/some/other/path/suffix2', + toRoutePath: '/testpath', + }, + + { + fromRoutePath: '/otherPath.html/some/path/suffix1', + toRoutePath: '/otherPath.html', + }, + { + fromRoutePath: '/otherPath.html/some/other/path/suffix2', + toRoutePath: '/otherPath.html', + }, + ]); + }); + + test('should filter unwanted redirects', () => { + expect( + collectRedirects( + createTestPluginContext( + { + fromExtensions: ['html', 'exe'], + toExtensions: ['html', 'exe'], + }, + [ + '/', + '/somePath', + '/somePath.html', + '/somePath.exe', + '/fromShouldWork.html', + '/toShouldWork', + ], + ), + ), + ).toEqual([ + { + fromRoutePath: '/toShouldWork.html', + toRoutePath: '/toShouldWork', + }, + { + fromRoutePath: '/toShouldWork.exe', + toRoutePath: '/toShouldWork', + }, + { + fromRoutePath: '/fromShouldWork', + toRoutePath: '/fromShouldWork.html', + }, + ]); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts index 435b832e5f..d0a21850d4 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/redirectCreators.test.ts @@ -44,11 +44,11 @@ const createExtensionValidationTests = ( }); }; -describe('fromExtensionsRedirectCreator', () => { - createExtensionValidationTests(fromExtensionsRedirectCreator); +describe('toExtensionsRedirectCreator', () => { + createExtensionValidationTests(toExtensionsRedirectCreator); test('should create redirects from html/htm extensions', () => { - const redirectCreator = fromExtensionsRedirectCreator(['html', 'htm']); + const redirectCreator = toExtensionsRedirectCreator(['html', 'htm']); expect(redirectCreator('')).toEqual([]); expect(redirectCreator('/')).toEqual([]); expect(redirectCreator('/abc.html')).toEqual(['/abc']); @@ -57,18 +57,18 @@ describe('fromExtensionsRedirectCreator', () => { }); test('should not create redirection for an empty extension array', () => { - const redirectCreator = fromExtensionsRedirectCreator([]); + const redirectCreator = toExtensionsRedirectCreator([]); expect(redirectCreator('')).toEqual([]); expect(redirectCreator('/')).toEqual([]); expect(redirectCreator('/abc.html')).toEqual([]); }); }); -describe('toExtensionsRedirectCreator', () => { - createExtensionValidationTests(toExtensionsRedirectCreator); +describe('fromExtensionsRedirectCreator', () => { + createExtensionValidationTests(fromExtensionsRedirectCreator); test('should create redirects to html/htm extensions', () => { - const redirectCreator = toExtensionsRedirectCreator(['html', 'htm']); + const redirectCreator = fromExtensionsRedirectCreator(['html', 'htm']); expect(redirectCreator('')).toEqual([]); expect(redirectCreator('/')).toEqual([]); expect(redirectCreator('/abc')).toEqual(['/abc.html', '/abc.htm']); @@ -77,7 +77,7 @@ describe('toExtensionsRedirectCreator', () => { }); test('should not create redirection for an empty extension array', () => { - const redirectCreator = toExtensionsRedirectCreator([]); + const redirectCreator = fromExtensionsRedirectCreator([]); expect(redirectCreator('')).toEqual([]); expect(redirectCreator('/')).toEqual([]); expect(redirectCreator('/abc')).toEqual([]); diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 67e27aabd9..8e7a11abdd 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -31,6 +31,8 @@ function filterUnwantedRedirects( redirects: RedirectMetadata[], pluginContext: PluginContext, ): RedirectMetadata[] { + // TODO how should we warn the user of filtered redirects? + // we don't want to create twice the same redirect redirects = uniqBy(redirects, (redirect) => redirect.fromRoutePath); diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index 2016fbe397..bc0b8d2a27 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -23,7 +23,7 @@ export default function pluginClientRedirectsPages( async postBuild(props: Props) { const pluginContext: PluginContext = { routesPaths: props.routesPaths, - siteConfig: props.siteConfig, + baseUrl: props.baseUrl, outDir: props.outDir, options, }; diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts index 6a9256b2d3..90c3917e44 100644 --- a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts +++ b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts @@ -38,7 +38,8 @@ const validateExtension = (ext: string) => { const addLeadingDot = (extension: string) => `.${extension}`; -export function fromExtensionsRedirectCreator( +// Create new /path that redirects to existing an /path.html +export function toExtensionsRedirectCreator( extensions: string[], ): RedirectsCreator { extensions.forEach(validateExtension); @@ -60,7 +61,8 @@ export function fromExtensionsRedirectCreator( }; } -export function toExtensionsRedirectCreator( +// Create new /path.html that redirects to existing an /path +export function fromExtensionsRedirectCreator( extensions: string[], ): RedirectsCreator { extensions.forEach(validateExtension); diff --git a/packages/docusaurus-plugin-client-redirects/src/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts index b5b542484a..e9a208074b 100644 --- a/packages/docusaurus-plugin-client-redirects/src/types.ts +++ b/packages/docusaurus-plugin-client-redirects/src/types.ts @@ -15,24 +15,24 @@ export type PluginOptions = { export type UserPluginOptions = Partial; +// The minimal infos the plugin needs to work +export type PluginContext = Pick< + Props, + 'routesPaths' | 'outDir' | 'baseUrl' +> & { + options: PluginOptions; +}; + // For a given existing route path, // return all the paths from which we should redirect from export type RedirectsCreator = ( routePath: string, ) => string[] | null | undefined; -// Having an in-memory representation of wanted redirects is easier to test +// In-memory representation of redirects we want: easier to test +// /!\ easy to be confused: "fromRoutePath" is the new page we should create, +// that redirects to "toRoutePath" the existing Docusaurus page export type RedirectMetadata = { fromRoutePath: string; toRoutePath: string; - toUrl: string; - redirectPageContent: string; - redirectAbsoluteFilePath: string; -}; - -export type PluginContext = Pick< - Props, - 'routesPaths' | 'siteConfig' | 'outDir' -> & { - options: PluginOptions; }; From e93b98a2426b0e7375cf3b53c4e57c66121cbb57 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 19:51:37 +0200 Subject: [PATCH 09/41] fix writeRedirectFiles --- .../src/collectRedirects.ts | 3 - .../src/index.ts | 2 +- .../src/writeRedirectFiles.ts | 59 +++++++++++++++---- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 8e7a11abdd..89b36f9c03 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import path from 'path'; import {flatten, uniqBy} from 'lodash'; import { RedirectsCreator, @@ -13,8 +12,6 @@ import { RedirectMetadata, PluginOptions, } from './types'; -import createRedirectPageContent from './createRedirectPageContent'; -import {addTrailingSlash, getFilePathForRoutePath} from './utils'; import { fromExtensionsRedirectCreator, toExtensionsRedirectCreator, diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index bc0b8d2a27..1253e2b768 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -31,7 +31,7 @@ export default function pluginClientRedirectsPages( const redirects: RedirectMetadata[] = collectRedirects(pluginContext); // Write files only at the end: make code more easy to test without IO - await writeRedirectFiles(redirects); + await writeRedirectFiles(redirects, pluginContext); }, }; } diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts index 7dbda45323..f98e5a8ccf 100644 --- a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -6,23 +6,58 @@ */ import fs from 'fs-extra'; -import {RedirectMetadata} from './types'; +import path from 'path'; +import {memoize} from 'lodash'; -type RedirectFile = Pick< - RedirectMetadata, - 'redirectAbsoluteFilePath' | 'redirectPageContent' ->; +import {PluginContext, RedirectMetadata} from './types'; +import createRedirectPageContent from './createRedirectPageContent'; +import {addTrailingSlash, getFilePathForRoutePath} from './utils'; -export default async function writeRedirectFiles(redirects: RedirectFile[]) { - async function writeRedirectFile(redirect: RedirectFile) { +type FileMetadata = RedirectMetadata & { + fileAbsolutePath: string; + fileContent: string; +}; + +function toFileMetadata( + redirects: RedirectMetadata[], + pluginContext: PluginContext, +): FileMetadata[] { + // 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}); + }); + + return redirects.map((redirect) => { + const fileAbsolutePath = path.join( + pluginContext.outDir, + getFilePathForRoutePath(redirect.fromRoutePath), + ); + const toUrl = addTrailingSlash( + `${pluginContext.baseUrl}${redirect.toRoutePath}`, + ); + const fileContent = createPageContentMemoized(toUrl); + return { + ...redirect, + fileAbsolutePath, + fileContent, + }; + }); +} + +export default async function writeRedirectFiles( + redirects: RedirectMetadata[], + pluginContext: PluginContext, +) { + async function writeFile(file: FileMetadata) { try { - await fs.writeFile( - redirect.redirectAbsoluteFilePath, - redirect.redirectPageContent, - ); + await fs.writeFile(file.fileAbsolutePath, file.fileContent); } catch (err) { throw new Error(`Redirect file creation error: ${err}`); } } - await Promise.all(redirects.map(writeRedirectFile)); + + const files = toFileMetadata(redirects, pluginContext); + await Promise.all(files.map(writeFile)); } From 0c330df5cc6be717e0599de846d150ab2a22cdf2 Mon Sep 17 00:00:00 2001 From: slorber Date: Mon, 25 May 2020 20:06:52 +0200 Subject: [PATCH 10/41] fix template import issue --- .../src/createRedirectPageContent.ts | 4 ++-- .../redirectPage.template.html.ts} | 2 +- website/docusaurus.config.js | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) rename packages/docusaurus-plugin-client-redirects/src/{template/redirectPage.html.template.js => templates/redirectPage.template.html.ts} (95%) diff --git a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts index d7b9452328..2275d66d8e 100644 --- a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts +++ b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -const eta = require('eta'); -const redirectPageTemplate = require('./template/redirectPage.html.template'); +import * as eta from 'eta'; +import redirectPageTemplate from './templates/redirectPage.template.html'; type CreateRedirectPageOptions = { toUrl: string; diff --git a/packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js b/packages/docusaurus-plugin-client-redirects/src/templates/redirectPage.template.html.ts similarity index 95% rename from packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js rename to packages/docusaurus-plugin-client-redirects/src/templates/redirectPage.template.html.ts index e36d306bb8..848ca2a8cb 100644 --- a/packages/docusaurus-plugin-client-redirects/src/template/redirectPage.html.template.js +++ b/packages/docusaurus-plugin-client-redirects/src/templates/redirectPage.template.html.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -module.exports = ` +export default ` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ca973602b7..e3777fa5f6 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -21,7 +21,12 @@ module.exports = { }, themes: ['@docusaurus/theme-live-codeblock'], plugins: [ - ['@docusaurus/plugin-client-redirects', {}], + [ + '@docusaurus/plugin-client-redirects', + { + fromExtensions: ['html'], + }, + ], [ '@docusaurus/plugin-ideal-image', { From 9a73680ea5ea33e1c1af3f7df3890af88612a7dd Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 19:03:02 +0200 Subject: [PATCH 11/41] fix missing ensureDir call --- .../docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts index f98e5a8ccf..1e723747ea 100644 --- a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -52,6 +52,7 @@ export default async function writeRedirectFiles( ) { async function writeFile(file: FileMetadata) { try { + await fs.ensureDir(path.dirname(file.fileAbsolutePath)); await fs.writeFile(file.fileAbsolutePath, file.fileContent); } catch (err) { throw new Error(`Redirect file creation error: ${err}`); From 5efbe90034a51e9ad1a8e1aa1a4bceccd9404d0b Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 19:05:11 +0200 Subject: [PATCH 12/41] prettierignore: packages/docusaurus-plugin-client-redirects/lib/ --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index e918d160e8..851882ce8d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ build 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/ From 8c8adc5e6b88b690ac0fe7a2eed0d6b5064accb7 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 19:12:19 +0200 Subject: [PATCH 13/41] fix annoying inline snapshot not working due to fmt/indentation issue --- .../normalizePluginOptions.test.ts.snap | 13 +++++++++++++ .../src/__tests__/normalizePluginOptions.test.ts | 14 +++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/normalizePluginOptions.test.ts.snap diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/normalizePluginOptions.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/normalizePluginOptions.test.ts.snap new file mode 100644 index 0000000000..9d53e37b50 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/normalizePluginOptions.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`normalizePluginOptions should reject bad createRedirects user inputs 1`] = `"createRedirects should be a function"`; + +exports[`normalizePluginOptions should reject bad fromExtensions user inputs 1`] = ` +"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()\`" +`; + +exports[`normalizePluginOptions should reject bad toExtensions user inputs 1`] = ` +"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()\`" +`; diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts index 6626b4a7e2..af0bf718c2 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/normalizePluginOptions.test.ts @@ -49,10 +49,7 @@ describe('normalizePluginOptions', () => { normalizePluginOptions({ fromExtensions: [null, undefined, 123, true] as any, }), - ).toThrowErrorMatchingInlineSnapshot(` - "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()\`" - `); + ).toThrowErrorMatchingSnapshot(); }); test('should reject bad toExtensions user inputs', () => { @@ -60,10 +57,7 @@ describe('normalizePluginOptions', () => { normalizePluginOptions({ toExtensions: [null, undefined, 123, true] as any, }), - ).toThrowErrorMatchingInlineSnapshot(` - "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()\`" - `); + ).toThrowErrorMatchingSnapshot(); }); test('should reject bad createRedirects user inputs', () => { @@ -71,8 +65,6 @@ describe('normalizePluginOptions', () => { normalizePluginOptions({ createRedirects: ['bad', 'value'] as any, }), - ).toThrowErrorMatchingInlineSnapshot( - `"createRedirects should be a function"`, - ); + ).toThrowErrorMatchingSnapshot(); }); }); From cb2b5a540774cc5e9169a089a514d4aee01184c2 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 20:04:22 +0200 Subject: [PATCH 14/41] add write files test --- .../writeRedirectFiles.test.ts.snap | 36 ++++++++ .../src/__tests__/writeRedirectFiles.test.ts | 88 +++++++++++++++++++ .../src/index.ts | 12 ++- .../src/writeRedirectFiles.ts | 54 ++++++++---- 4 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap create mode 100644 packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap new file mode 100644 index 0000000000..fb7eebf4c2 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`toRedirectFilesMetadata should create appropriate metadatas: fileContent 1`] = ` +Array [ + " + + + + + + +", + " + + + + + + +", + " + + + + + + +", +] +`; diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts new file mode 100644 index 0000000000..f23fbe8cd1 --- /dev/null +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts @@ -0,0 +1,88 @@ +/** + * 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( + [ + {fromRoutePath: '/abc.html', toRoutePath: '/abc'}, + {fromRoutePath: '/def', toRoutePath: '/def.html'}, + {fromRoutePath: '/xyz', toRoutePath: '/'}, + ], + 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', + ); + }); +}); + +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`, + ); + }); +}); diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index 1253e2b768..c94b734514 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -10,7 +10,10 @@ import {UserPluginOptions, PluginContext, RedirectMetadata} from './types'; import normalizePluginOptions from './normalizePluginOptions'; import collectRedirects from './collectRedirects'; -import writeRedirectFiles from './writeRedirectFiles'; +import writeRedirectFiles, { + toRedirectFilesMetadata, + RedirectFileMetadata, +} from './writeRedirectFiles'; export default function pluginClientRedirectsPages( _context: LoadContext, @@ -30,8 +33,13 @@ export default function pluginClientRedirectsPages( 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(redirects, pluginContext); + await writeRedirectFiles(redirectFiles); }, }; } diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts index 1e723747ea..e8d099b662 100644 --- a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -13,15 +13,17 @@ import {PluginContext, RedirectMetadata} from './types'; import createRedirectPageContent from './createRedirectPageContent'; import {addTrailingSlash, getFilePathForRoutePath} from './utils'; -type FileMetadata = RedirectMetadata & { +export type WriteFilesPluginContext = Pick; + +export type RedirectFileMetadata = { fileAbsolutePath: string; fileContent: string; }; -function toFileMetadata( +export function toRedirectFilesMetadata( redirects: RedirectMetadata[], - pluginContext: PluginContext, -): FileMetadata[] { + 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! @@ -29,7 +31,7 @@ function toFileMetadata( return createRedirectPageContent({toUrl}); }); - return redirects.map((redirect) => { + const createFileMetadata = (redirect: RedirectMetadata) => { const fileAbsolutePath = path.join( pluginContext.outDir, getFilePathForRoutePath(redirect.fromRoutePath), @@ -43,22 +45,36 @@ function toFileMetadata( 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( - redirects: RedirectMetadata[], - pluginContext: PluginContext, + redirectFiles: RedirectFileMetadata[], ) { - async function writeFile(file: FileMetadata) { - try { - await fs.ensureDir(path.dirname(file.fileAbsolutePath)); - await fs.writeFile(file.fileAbsolutePath, file.fileContent); - } catch (err) { - throw new Error(`Redirect file creation error: ${err}`); - } - } - - const files = toFileMetadata(redirects, pluginContext); - await Promise.all(files.map(writeFile)); + await Promise.all(redirectFiles.map(writeRedirectFile)); } From 9a2129551e221834503c03c8d3d18d976d2f99a0 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 20:16:40 +0200 Subject: [PATCH 15/41] fix bad redirection url --- .../writeRedirectFiles.test.ts.snap | 45 +++++++++++++++++++ .../src/__tests__/writeRedirectFiles.test.ts | 28 ++++++++++++ .../src/writeRedirectFiles.ts | 10 ++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap index fb7eebf4c2..3f0cb3c0d5 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap @@ -1,5 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`toRedirectFilesMetadata should create appropriate metadatas for empty baseUrl: fileContent baseUrl=empty 1`] = ` +Array [ + " + + + + + + +", +] +`; + +exports[`toRedirectFilesMetadata should create appropriate metadatas for root baseUrl: fileContent baseUrl=/ 1`] = ` +Array [ + " + + + + + + +", +] +`; + +exports[`toRedirectFilesMetadata should create appropriate metadatas for root baseUrl: fileContent baseUrl=empty 1`] = ` +Array [ + " + + + + + + +", +] +`; + exports[`toRedirectFilesMetadata should create appropriate metadatas: fileContent 1`] = ` Array [ " diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts index f23fbe8cd1..070d6db3e3 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts @@ -37,6 +37,34 @@ describe('toRedirectFilesMetadata', () => { 'fileContent', ); }); + + test('should create appropriate metadatas for root baseUrl', async () => { + const pluginContext = { + outDir: '/tmp/someFixedOutDir', + baseUrl: '/', + }; + const redirectFiles = toRedirectFilesMetadata( + [{fromRoutePath: '/abc.html', toRoutePath: '/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( + [{fromRoutePath: '/abc.html', toRoutePath: '/abc'}], + pluginContext, + ); + expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot( + 'fileContent baseUrl=empty', + ); + }); }); describe('writeRedirectFiles', () => { diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts index e8d099b662..19f40ddc7b 100644 --- a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -11,7 +11,11 @@ import {memoize} from 'lodash'; import {PluginContext, RedirectMetadata} from './types'; import createRedirectPageContent from './createRedirectPageContent'; -import {addTrailingSlash, getFilePathForRoutePath} from './utils'; +import { + addTrailingSlash, + getFilePathForRoutePath, + removeTrailingSlash, +} from './utils'; export type WriteFilesPluginContext = Pick; @@ -37,7 +41,9 @@ export function toRedirectFilesMetadata( getFilePathForRoutePath(redirect.fromRoutePath), ); const toUrl = addTrailingSlash( - `${pluginContext.baseUrl}${redirect.toRoutePath}`, + `${removeTrailingSlash(pluginContext.baseUrl)}${path.join( + redirect.toRoutePath, + )}`, ); const fileContent = createPageContentMemoized(toUrl); return { From cc1705e00cdcb3367de47ccd4c5b36cab1bd7627 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 26 May 2020 21:03:18 +0200 Subject: [PATCH 16/41] add canonical url --- .../writeRedirectFiles.test.ts.snap | 20 +++++-------------- .../templates/redirectPage.template.html.ts | 1 + 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap index 3f0cb3c0d5..df8a037778 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap @@ -7,6 +7,7 @@ Array [ + -", -] -`; - -exports[`toRedirectFilesMetadata should create appropriate metadatas for root baseUrl: fileContent baseUrl=empty 1`] = ` -Array [ - " - - - - +