"use strict"; /** * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const lodash_1 = tslib_1.__importDefault(require("lodash")); const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger")); const utils_1 = require("@docusaurus/utils"); const utils_common_1 = require("@docusaurus/utils-common"); const extensionRedirects_1 = require("./extensionRedirects"); const redirectValidation_1 = require("./redirectValidation"); function collectRedirects(pluginContext, trailingSlash) { // For each plugin config option, create the appropriate redirects const redirects = [ ...(0, extensionRedirects_1.createFromExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.fromExtensions), ...(0, extensionRedirects_1.createToExtensionsRedirects)(pluginContext.relativeRoutesPaths, pluginContext.options.toExtensions), ...createRedirectsOptionRedirects(pluginContext.options.redirects), ...createCreateRedirectsOptionRedirects(pluginContext.relativeRoutesPaths, pluginContext.options.createRedirects), ].map((redirect) => ({ ...redirect, // Given a redirect with `to: "/abc"` and `trailingSlash` enabled: // // - We don't want to reject `to: "/abc"`, as that unambiguously points to // `/abc/` now; // - We want to redirect `to: /abc/` without the user having to change all // her redirect plugin options // // It should be easy to toggle `trailingSlash` option without having to // change other configs to: (0, utils_common_1.applyTrailingSlash)(redirect.to, { trailingSlash, baseUrl: pluginContext.baseUrl, }), })); validateCollectedRedirects(redirects, pluginContext); return filterUnwantedRedirects(redirects, pluginContext); } exports.default = collectRedirects; function validateCollectedRedirects(redirects, pluginContext) { const redirectValidationErrors = redirects .map((redirect) => { try { (0, redirectValidation_1.validateRedirect)(redirect); return undefined; } catch (err) { return err.message; } }) .filter(Boolean); if (redirectValidationErrors.length > 0) { throw new Error(`Some created redirects are invalid: - ${redirectValidationErrors.join('\n- ')} `); } const allowedToPaths = pluginContext.relativeRoutesPaths.map((p) => decodeURI(p)); const toPaths = redirects .map((redirect) => redirect.to) // We now allow "to" to contain any string // We only do this "broken redirect" check from to that looks like pathnames // note: we allow querystring/anchors // See https://github.com/facebook/docusaurus/issues/6845 .map((to) => { if (to.startsWith('/')) { try { return decodeURI(new URL(to, 'https://example.com').pathname); } catch (e) { } } return undefined; }) .filter((to) => typeof to !== 'undefined'); const trailingSlashConfig = pluginContext.siteConfig.trailingSlash; // Key is the path, value is whether a valid toPath with a different trailing // slash exists; if the key doesn't exist it means it's valid const differByTrailSlash = new Map(toPaths.map((path) => [path, false])); allowedToPaths.forEach((toPath) => { if (differByTrailSlash.has(toPath)) { differByTrailSlash.delete(toPath); } else if (differByTrailSlash.has((0, utils_1.removeTrailingSlash)(toPath))) { if (trailingSlashConfig === true) { differByTrailSlash.set((0, utils_1.removeTrailingSlash)(toPath), true); } else { differByTrailSlash.delete((0, utils_1.removeTrailingSlash)(toPath)); } } else if (differByTrailSlash.has((0, utils_1.addTrailingSlash)(toPath))) { if (trailingSlashConfig === false) { differByTrailSlash.set((0, utils_1.addTrailingSlash)(toPath), true); } else { differByTrailSlash.delete((0, utils_1.addTrailingSlash)(toPath)); } } }); if (differByTrailSlash.size > 0) { const errors = Array.from(differByTrailSlash.entries()); let message = 'You are trying to create client-side redirections to invalid paths.\n'; const [trailingSlashIssues, invalidPaths] = lodash_1.default.partition(errors, ([, differ]) => differ); if (trailingSlashIssues.length) { message += ` These paths do exist, but because you have explicitly set trailingSlash=${trailingSlashConfig}, you need to write the path ${trailingSlashConfig ? 'with trailing slash' : 'without trailing slash'}: - ${trailingSlashIssues.map(([p]) => p).join('\n- ')} `; } if (invalidPaths.length) { message += ` These paths are redirected to but do not exist: - ${invalidPaths.map(([p]) => p).join('\n- ')} Valid paths you can redirect to: - ${allowedToPaths.join('\n- ')} `; } throw new Error(message); } } function filterUnwantedRedirects(redirects, pluginContext) { // We don't want to create the same redirect twice, since that would lead to // writing the same html redirection file twice. Object.entries(lodash_1.default.groupBy(redirects, (redirect) => redirect.from)).forEach(([from, groupedFromRedirects]) => { if (groupedFromRedirects.length > 1) { logger_1.default.report(pluginContext.siteConfig.onDuplicateRoutes) `name=${'@docusaurus/plugin-client-redirects'}: multiple redirects are created with the same "from" pathname: path=${from} It is not possible to redirect the same pathname to multiple destinations:${groupedFromRedirects.map((r) => JSON.stringify(r))}`; } }); const collectedRedirects = lodash_1.default.uniqBy(redirects, (redirect) => redirect.from); const { false: newRedirects = [], true: redirectsOverridingExistingPath = [] } = lodash_1.default.groupBy(collectedRedirects, (redirect) => pluginContext.relativeRoutesPaths.includes(redirect.from)); if (redirectsOverridingExistingPath.length > 0) { logger_1.default.report(pluginContext.siteConfig.onDuplicateRoutes) `name=${'@docusaurus/plugin-client-redirects'}: some redirects would override existing paths, and will be ignored:${redirectsOverridingExistingPath.map((r) => JSON.stringify(r))}`; } return newRedirects; } function createRedirectsOptionRedirects(redirectsOption) { // For convenience, user can use a string or a string[] function optionToRedirects(option) { if (typeof option.from === 'string') { return [{ from: option.from, to: option.to }]; } return option.from.map((from) => ({ from, to: option.to })); } return redirectsOption.flatMap(optionToRedirects); } // Create redirects from the "createRedirects" fn provided by the user function createCreateRedirectsOptionRedirects(paths, createRedirects) { function createPathRedirects(path) { const fromsMixed = createRedirects?.(path) ?? []; const froms = typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed; return froms.map((from) => ({ from, to: path })); } return paths.flatMap(createPathRedirects); }