fix(v2): redirect plugin: use siteConfig.trailingSlash (#4988)

This commit is contained in:
Sébastien Lorber 2021-06-16 19:04:28 +02:00 committed by GitHub
parent 80b6d9728e
commit b54ec72389
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 443 additions and 73 deletions

View file

@ -20,6 +20,7 @@
"@docusaurus/core": "2.0.0-beta.0",
"@docusaurus/types": "2.0.0-beta.0",
"@docusaurus/utils": "2.0.0-beta.0",
"@docusaurus/utils-common": "2.0.0-beta.0",
"@docusaurus/utils-validation": "2.0.0-beta.0",
"chalk": "^4.1.1",
"eta": "^1.11.0",

View file

@ -32,7 +32,83 @@ Array [
]
`;
exports[`toRedirectFilesMetadata should create appropriate metadatas: fileContent 1`] = `
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=false: fileContent 1`] = `
Array [
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/abc\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/abc\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/abc';
</script>
</html>",
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/def.html\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/def.html\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/def.html';
</script>
</html>",
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/';
</script>
</html>",
]
`;
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=true: fileContent 1`] = `
Array [
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/abc\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/abc\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/abc';
</script>
</html>",
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/def.html\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/def.html\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/def.html';
</script>
</html>",
"<!DOCTYPE html>
<html>
<head>
<meta charset=\\"UTF-8\\">
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
</head>
<script>
window.location.href = 'https://docusaurus.io/';
</script>
</html>",
]
`;
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=undefined: fileContent 1`] = `
Array [
"<!DOCTYPE html>
<html>

View file

@ -25,12 +25,17 @@ function createTestPluginContext(
describe('collectRedirects', () => {
test('should collect no redirect for undefined config', () => {
expect(
collectRedirects(createTestPluginContext(undefined, ['/', '/path'])),
collectRedirects(
createTestPluginContext(undefined, ['/', '/path']),
undefined,
),
).toEqual([]);
});
test('should collect no redirect for empty config', () => {
expect(collectRedirects(createTestPluginContext({}))).toEqual([]);
expect(collectRedirects(createTestPluginContext({}), undefined)).toEqual(
[],
);
});
test('should collect redirects to html/exe extension', () => {
@ -42,6 +47,7 @@ describe('collectRedirects', () => {
},
['/', '/somePath', '/otherPath.html'],
),
undefined,
),
).toEqual([
{
@ -64,6 +70,7 @@ describe('collectRedirects', () => {
},
['/', '/somePath', '/otherPath.html'],
),
undefined,
),
).toEqual([
{
@ -91,6 +98,79 @@ describe('collectRedirects', () => {
},
['/', '/somePath'],
),
undefined,
),
).toEqual([
{
from: '/someLegacyPath',
to: '/somePath',
},
{
from: '/someLegacyPathArray1',
to: '/',
},
{
from: '/someLegacyPathArray2',
to: '/',
},
]);
});
test('should collect redirects from plugin option redirects with trailingSlash=true', () => {
expect(
collectRedirects(
createTestPluginContext(
{
redirects: [
{
from: '/someLegacyPath',
to: '/somePath',
},
{
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
to: '/',
},
],
},
['/', '/somePath/'],
),
true,
),
).toEqual([
{
from: '/someLegacyPath',
to: '/somePath/',
},
{
from: '/someLegacyPathArray1',
to: '/',
},
{
from: '/someLegacyPathArray2',
to: '/',
},
]);
});
test('should collect redirects from plugin option redirects with trailingSlash=false', () => {
expect(
collectRedirects(
createTestPluginContext(
{
redirects: [
{
from: '/someLegacyPath',
to: '/somePath/',
},
{
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
to: '/',
},
],
},
['/', '/somePath'],
),
false,
),
).toEqual([
{
@ -130,6 +210,7 @@ describe('collectRedirects', () => {
},
['/', '/someExistingPath', '/anotherExistingPath'],
),
undefined,
),
).toThrowErrorMatchingSnapshot();
});
@ -148,6 +229,7 @@ describe('collectRedirects', () => {
},
['/', '/testpath', '/otherPath.html'],
),
undefined,
),
).toEqual([
{
@ -197,6 +279,7 @@ describe('collectRedirects', () => {
},
['/'],
),
undefined,
),
).toThrowErrorMatchingSnapshot();
});
@ -215,6 +298,7 @@ describe('collectRedirects', () => {
},
['/'],
),
undefined,
),
).toThrowErrorMatchingSnapshot();
});
@ -236,6 +320,7 @@ describe('collectRedirects', () => {
'/toShouldWork',
],
),
undefined,
),
).toEqual([
{

View file

@ -42,7 +42,7 @@ describe('createToUrl', () => {
});
describe('toRedirectFilesMetadata', () => {
test('should create appropriate metadatas', async () => {
test('should create appropriate metadatas trailingSlash=undefined', async () => {
const pluginContext = {
outDir: '/tmp/someFixedOutDir',
baseUrl: 'https://docusaurus.io',
@ -55,10 +55,11 @@ describe('toRedirectFilesMetadata', () => {
{from: '/xyz', to: '/'},
],
pluginContext,
undefined,
);
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
path.join(pluginContext.outDir, '/abc.html/index.html'),
path.join(pluginContext.outDir, '/abc.html'),
path.join(pluginContext.outDir, '/def/index.html'),
path.join(pluginContext.outDir, '/xyz/index.html'),
]);
@ -68,6 +69,60 @@ describe('toRedirectFilesMetadata', () => {
);
});
test('should create appropriate metadatas trailingSlash=true', async () => {
const pluginContext = {
outDir: '/tmp/someFixedOutDir',
baseUrl: 'https://docusaurus.io',
};
const redirectFiles = toRedirectFilesMetadata(
[
{from: '/abc.html', to: '/abc'},
{from: '/def', to: '/def.html'},
{from: '/xyz', to: '/'},
],
pluginContext,
true,
);
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
path.join(pluginContext.outDir, '/abc.html'),
path.join(pluginContext.outDir, '/def/index.html'),
path.join(pluginContext.outDir, '/xyz/index.html'),
]);
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
'fileContent',
);
});
test('should create appropriate metadatas trailingSlash=false', async () => {
const pluginContext = {
outDir: '/tmp/someFixedOutDir',
baseUrl: 'https://docusaurus.io',
};
const redirectFiles = toRedirectFilesMetadata(
[
{from: '/abc.html', to: '/abc'},
{from: '/def', to: '/def.html'},
{from: '/xyz', to: '/'},
],
pluginContext,
false,
);
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
path.join(pluginContext.outDir, '/abc.html'),
path.join(pluginContext.outDir, '/def.html'),
path.join(pluginContext.outDir, '/xyz.html'),
]);
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
'fileContent',
);
});
test('should create appropriate metadatas for root baseUrl', async () => {
const pluginContext = {
outDir: '/tmp/someFixedOutDir',
@ -76,6 +131,7 @@ describe('toRedirectFilesMetadata', () => {
const redirectFiles = toRedirectFilesMetadata(
[{from: '/abc.html', to: '/abc'}],
pluginContext,
undefined,
);
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
'fileContent baseUrl=/',
@ -90,6 +146,7 @@ describe('toRedirectFilesMetadata', () => {
const redirectFiles = toRedirectFilesMetadata(
[{from: '/abc.html', to: '/abc'}],
pluginContext,
undefined,
);
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
'fileContent baseUrl=empty',

View file

@ -17,17 +17,36 @@ import {
createToExtensionsRedirects,
} from './extensionRedirects';
import {validateRedirect} from './redirectValidation';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import chalk from 'chalk';
export default function collectRedirects(
pluginContext: PluginContext,
trailingSlash: boolean | undefined,
): RedirectMetadata[] {
const redirects = doCollectRedirects(pluginContext);
let redirects = doCollectRedirects(pluginContext);
redirects = applyRedirectsTrailingSlash(redirects, trailingSlash);
validateCollectedRedirects(redirects, pluginContext);
return filterUnwantedRedirects(redirects, pluginContext);
}
// If users wants to redirect to=/abc and they enable trailingSlash=true then
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid path now)
// => we want to redirect to=/abc/ without the user having to change all its redirect plugin options
// It should be easy to toggle siteConfig.trailingSlash option without having to change other configs
function applyRedirectsTrailingSlash(
redirects: RedirectMetadata[],
trailingSlash: boolean | undefined,
) {
return redirects.map((redirect) => {
return {
...redirect,
to: applyTrailingSlash(redirect.to, trailingSlash),
};
});
}
function validateCollectedRedirects(
redirects: RedirectMetadata[],
pluginContext: PluginContext,

View file

@ -15,13 +15,28 @@ import writeRedirectFiles, {
RedirectFileMetadata,
} from './writeRedirectFiles';
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
import chalk from 'chalk';
export default function pluginClientRedirectsPages(
_context: LoadContext,
context: LoadContext,
opts: UserPluginOptions,
): Plugin<unknown> {
const {trailingSlash} = context.siteConfig;
const options = normalizePluginOptions(opts);
// Special case, when using trailingSlash=false we output /xyz.html files instead of /xyz/index.html
// It makes no sense to use option fromExtensions=["html"]: the redirect files would be the same as the original files
if (options.fromExtensions.includes('html') && trailingSlash === false) {
console.warn(
chalk.yellow(`Using the Docusaurus redirect plugin with fromExtensions=['html'] and siteConfig.trailingSlash=false is prevented.
It would lead the redirect plugin to override all the page.html files created by Docusaurus.`),
);
options.fromExtensions = options.fromExtensions.filter(
(ext) => ext !== 'html',
);
}
return {
name: 'docusaurus-plugin-client-redirects',
async postBuild(props: Props) {
@ -34,11 +49,15 @@ export default function pluginClientRedirectsPages(
options,
};
const redirects: RedirectMetadata[] = collectRedirects(pluginContext);
const redirects: RedirectMetadata[] = collectRedirects(
pluginContext,
trailingSlash,
);
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
redirects,
pluginContext,
trailingSlash,
);
// Write files only at the end: make code more easy to test without IO

View file

@ -27,6 +27,7 @@ export function createToUrl(baseUrl: string, to: string): string {
export function toRedirectFilesMetadata(
redirects: RedirectMetadata[],
pluginContext: WriteFilesPluginContext,
trailingSlash: boolean | undefined,
): RedirectFileMetadata[] {
// Perf: avoid rendering the template twice with the exact same "props"
// We might create multiple redirect pages for the same destination url
@ -36,10 +37,8 @@ export function toRedirectFilesMetadata(
});
const createFileMetadata = (redirect: RedirectMetadata) => {
const fileAbsolutePath = path.join(
pluginContext.outDir,
getFilePathForRoutePath(redirect.from),
);
const filePath = getFilePathForRoutePath(redirect.from, trailingSlash);
const fileAbsolutePath = path.join(pluginContext.outDir, filePath);
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
const fileContent = createPageContentMemoized(toUrl);
return {

View file

@ -14,10 +14,20 @@ describe('applyTrailingSlash', () => {
expect(applyTrailingSlash('', undefined)).toEqual('');
});
test('should apply to /', () => {
test('should not apply to /', () => {
expect(applyTrailingSlash('/', true)).toEqual('/');
expect(applyTrailingSlash('/', false)).toEqual('');
expect(applyTrailingSlash('/', false)).toEqual('/');
expect(applyTrailingSlash('/', undefined)).toEqual('/');
expect(applyTrailingSlash('/?query#anchor', true)).toEqual(
'/?query#anchor',
);
expect(applyTrailingSlash('/?query#anchor', false)).toEqual(
'/?query#anchor',
);
expect(applyTrailingSlash('/?query#anchor', undefined)).toEqual(
'/?query#anchor',
);
});
test('should not apply to #anchor links ', () => {

View file

@ -9,8 +9,8 @@ export default function applyTrailingSlash(
path: string,
trailingSlash: boolean | undefined,
): string {
// Never apply trailing slash to an anchor link
if (path.startsWith('#')) {
// Never apply trailing slash to an anchor link
return path;
}
@ -29,7 +29,12 @@ export default function applyTrailingSlash(
// The trailing slash should be handled before the ?search#hash !
const [pathname] = path.split(/[#?]/);
const newPathname = trailingSlash
// Never transform '/' to ''
const newPathname =
pathname === '/'
? '/'
: trailingSlash
? addTrailingSlash(pathname)
: removeTrailingSlash(pathname);
return path.replace(pathname, newPathname);

View file

@ -0,0 +1,87 @@
/**
* 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 {getFilePathForRoutePath} from '../getFilePathForRoutePath';
import {posixPath} from '../posixPath';
describe('getFilePathForRoutePath trailingSlash=undefined', () => {
test('works for /', () => {
expect(posixPath(getFilePathForRoutePath('/', undefined))).toEqual(
'/index.html',
);
});
test('works for /somePath', () => {
expect(posixPath(getFilePathForRoutePath('/somePath', undefined))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/', () => {
expect(posixPath(getFilePathForRoutePath('/somePath/', undefined))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/xyz.html', () => {
expect(
posixPath(getFilePathForRoutePath('/somePath/xyz.html', undefined)),
).toEqual('/somePath/xyz.html');
});
});
describe('getFilePathForRoutePath trailingSlash=true', () => {
test('works for /', () => {
expect(posixPath(getFilePathForRoutePath('/', true))).toEqual(
'/index.html',
);
});
test('works for /somePath', () => {
expect(posixPath(getFilePathForRoutePath('/somePath', true))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/', () => {
expect(posixPath(getFilePathForRoutePath('/somePath/', true))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/xyz.html', () => {
expect(
posixPath(getFilePathForRoutePath('/somePath/xyz.html', true)),
).toEqual('/somePath/xyz.html');
});
});
describe('getFilePathForRoutePath trailingSlash=false', () => {
test('works for /', () => {
expect(posixPath(getFilePathForRoutePath('/', false))).toEqual(
'/index.html',
);
});
test('works for /somePath', () => {
expect(posixPath(getFilePathForRoutePath('/somePath', false))).toEqual(
'/somePath.html',
);
});
test('works for /somePath/', () => {
expect(posixPath(getFilePathForRoutePath('/somePath/', false))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/xyz.html', () => {
expect(
posixPath(getFilePathForRoutePath('/somePath/xyz.html', false)),
).toEqual('/somePath/xyz.html');
});
});

View file

@ -21,7 +21,6 @@ import {
removeTrailingSlash,
removeSuffix,
removePrefix,
getFilePathForRoutePath,
addLeadingSlash,
getElementsAround,
mergeTranslations,
@ -401,22 +400,6 @@ describe('removePrefix', () => {
});
});
describe('getFilePathForRoutePath', () => {
test('works for /', () => {
expect(posixPath(getFilePathForRoutePath('/'))).toEqual('/index.html');
});
test('works for /somePath', () => {
expect(posixPath(getFilePathForRoutePath('/somePath'))).toEqual(
'/somePath/index.html',
);
});
test('works for /somePath/', () => {
expect(posixPath(getFilePathForRoutePath('/somePath/'))).toEqual(
'/somePath/index.html',
);
});
});
describe('getElementsAround', () => {
test('can return elements around', () => {
expect(getElementsAround(['a', 'b', 'c', 'd'], 0)).toEqual({

View file

@ -0,0 +1,43 @@
/**
* 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 getFilePathForRoutePath(routePath: string): string {
const fileName = path.basename(routePath);
const filePath = path.dirname(routePath);
return path.join(filePath, `${fileName}/index.html`);
}
*/
// Almost exact copy of the behavior we implemented in our Docusaurus fork of the webpack static gen plugin
// See https://github.com/slorber/static-site-generator-webpack-plugin/blob/master/index.js#L167
export function getFilePathForRoutePath(
routePath: string,
trailingSlash: boolean | undefined,
): string {
// const outputFileName = routePath.replace(/^(\/|\\)/, ''); // Remove leading slashes for webpack-dev-server
// Paths ending with .html are left untouched
if (/\.(html?)$/i.test(routePath)) {
return routePath;
}
// Legacy retro-compatible behavior
if (typeof trailingSlash === 'undefined') {
return path.join(routePath, 'index.html');
}
// New behavior: we can say if we prefer file/folder output
// Useful resource: https://github.com/slorber/trailing-slash-guide
if (routePath === '' || routePath.endsWith('/') || trailingSlash) {
return path.join(routePath, 'index.html');
} else {
return `${routePath}.html`;
}
}

View file

@ -27,6 +27,7 @@ import {docuHash} from './docuHash';
export const posixPath = posixPathImport;
export * from './getFilePathForRoutePath';
export * from './codeTranslationsUtils';
export * from './markdownParser';
export * from './markdownLinks';
@ -325,12 +326,6 @@ export function removePrefix(str: string, prefix: string): string {
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
}
export function getFilePathForRoutePath(routePath: string): string {
const fileName = path.basename(routePath);
const filePath = path.dirname(routePath);
return path.join(filePath, `${fileName}/index.html`);
}
export function getElementsAround<T extends unknown>(
array: T[],
aroundIndex: number,

View file

@ -12,11 +12,6 @@ export default function applyRouteTrailingSlash(
route: RouteConfig,
trailingSlash: boolean | undefined,
) {
// Never transform "/" to "" => cause router issues ("" catch everything)
if (route.path === '/') {
return route;
}
return {
...route,
path: applyTrailingSlash(route.path, trailingSlash),

View file

@ -132,11 +132,7 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging;
],
[
'@docusaurus/plugin-client-redirects',
isDeployPreview
? // Plugin is disabled for deploy preview because we use trailing slashes on deploy previews
// This plugin is sensitive to trailing slashes, and we don't care much about making it work on deploy previews
{}
: {
{
fromExtensions: ['html'],
createRedirects: function (path) {
// redirect to /docs from /docs/introduction,