From 85a79fd9b96c82ca7cde162d7e5b77a0bac95697 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Mon, 28 Mar 2022 17:12:36 +0800 Subject: [PATCH] refactor(core): reorganize functions (#7037) --- .../src/__tests__/index.test.ts | 2 +- .../src/__tests__/i18nUtils.test.ts | 84 ++++++ packages/docusaurus-utils/src/i18nUtils.ts | 50 +++- packages/docusaurus-utils/src/index.ts | 1 + packages/docusaurus/src/commands/build.ts | 2 +- packages/docusaurus/src/commands/clear.ts | 2 +- packages/docusaurus/src/commands/deploy.ts | 4 +- packages/docusaurus/src/commands/external.ts | 9 +- packages/docusaurus/src/commands/serve.ts | 4 +- packages/docusaurus/src/commands/start.ts | 2 +- .../commands/swizzle/__tests__/index.test.ts | 2 +- .../src/commands/swizzle/context.ts | 12 +- .../docusaurus/src/commands/swizzle/index.ts | 2 +- .../src/commands/writeHeadingIds.ts | 12 +- .../src/commands/writeTranslations.ts | 12 +- packages/docusaurus/src/index.ts | 30 +-- .../siteMetadata}/dummy-plugin.js | 0 .../__fixtures__/siteMetadata}/package.json | 0 .../server/__tests__/clientModules.test.ts | 107 ++++++++ .../src/server/__tests__/config.test.ts | 2 +- .../src/server/__tests__/htmlTags.test.ts | 224 ++++++++++++++++ .../src/server/__tests__/i18n.test.ts | 85 +----- .../src/server/__tests__/routes.test.ts | 2 +- .../siteMetadata.test.ts} | 10 +- .../__tests__/__fixtures__/plugin-empty.js | 13 - .../__tests__/__fixtures__/plugin-foo-bar.js | 16 -- .../__fixtures__/plugin-hello-world.js | 16 -- .../client-modules/__tests__/index.test.ts | 91 ------- .../index.ts => clientModules.ts} | 4 +- packages/docusaurus/src/server/config.ts | 2 +- .../__tests__/__fixtures__/plugin-empty.js | 12 - .../__tests__/__fixtures__/plugin-headTags.js | 26 -- .../__fixtures__/plugin-postBodyTags.js | 22 -- .../__fixtures__/plugin-preBodyTags.js | 23 -- .../html-tags/__tests__/htmlTags.test.ts | 122 --------- .../server/html-tags/__tests__/index.test.ts | 92 ------- .../src/server/html-tags/htmlTags.ts | 50 ---- .../docusaurus/src/server/html-tags/index.ts | 52 ---- packages/docusaurus/src/server/htmlTags.ts | 81 ++++++ packages/docusaurus/src/server/i18n.ts | 30 +-- packages/docusaurus/src/server/index.ts | 243 +----------------- .../__fixtures__/presets}/preset-mixed.js | 0 .../__fixtures__/presets}/preset-plugins.js | 0 .../__fixtures__/presets}/preset-themes.js | 0 .../__snapshots__/index.test.ts.snap | 129 ++-------- .../__snapshots__/presets.test.ts.snap} | 0 .../__snapshots__/routeConfig.test.ts.snap | 105 ++++++++ .../server/plugins/__tests__/index.test.ts | 152 +++-------- .../src/server/plugins/__tests__/init.test.ts | 14 +- .../__tests__/presets.test.ts} | 30 ++- ...ilingSlash.test.ts => routeConfig.test.ts} | 88 ++++++- .../server/plugins/applyRouteTrailingSlash.ts | 27 -- .../docusaurus/src/server/plugins/configs.ts | 55 ++++ .../docusaurus/src/server/plugins/index.ts | 64 +---- .../docusaurus/src/server/plugins/init.ts | 14 +- .../{presets/index.ts => plugins/presets.ts} | 2 +- .../src/server/plugins/routeConfig.ts | 67 +++++ .../src/server/plugins/synthetic.ts | 129 ++++++++++ packages/docusaurus/src/server/routes.ts | 2 +- .../{versions/index.ts => siteMetadata.ts} | 58 ++++- .../src/server/themes/__tests__/alias.test.ts | 2 +- .../docusaurus/src/server/themes/alias.ts | 2 +- .../docusaurus/src/server/themes/index.ts | 4 +- .../src/server/translations/translations.ts | 12 +- 64 files changed, 1207 insertions(+), 1304 deletions(-) rename packages/docusaurus/src/server/{versions/__tests__/__fixtures__ => __tests__/__fixtures__/siteMetadata}/dummy-plugin.js (100%) rename packages/docusaurus/src/server/{versions/__tests__/__fixtures__ => __tests__/__fixtures__/siteMetadata}/package.json (100%) create mode 100644 packages/docusaurus/src/server/__tests__/clientModules.test.ts create mode 100644 packages/docusaurus/src/server/__tests__/htmlTags.test.ts rename packages/docusaurus/src/server/{versions/__tests__/index.test.ts => __tests__/siteMetadata.test.ts} (73%) delete mode 100644 packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-empty.js delete mode 100644 packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-foo-bar.js delete mode 100644 packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-hello-world.js delete mode 100644 packages/docusaurus/src/server/client-modules/__tests__/index.test.ts rename packages/docusaurus/src/server/{client-modules/index.ts => clientModules.ts} (81%) delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-empty.js delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-headTags.js delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-postBodyTags.js delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-preBodyTags.js delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/htmlTags.test.ts delete mode 100644 packages/docusaurus/src/server/html-tags/__tests__/index.test.ts delete mode 100644 packages/docusaurus/src/server/html-tags/htmlTags.ts delete mode 100644 packages/docusaurus/src/server/html-tags/index.ts create mode 100644 packages/docusaurus/src/server/htmlTags.ts rename packages/docusaurus/src/server/{presets/__tests__/__fixtures__ => plugins/__tests__/__fixtures__/presets}/preset-mixed.js (100%) rename packages/docusaurus/src/server/{presets/__tests__/__fixtures__ => plugins/__tests__/__fixtures__/presets}/preset-plugins.js (100%) rename packages/docusaurus/src/server/{presets/__tests__/__fixtures__ => plugins/__tests__/__fixtures__/presets}/preset-themes.js (100%) rename packages/docusaurus/src/server/{presets/__tests__/__snapshots__/index.test.ts.snap => plugins/__tests__/__snapshots__/presets.test.ts.snap} (100%) create mode 100644 packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap rename packages/docusaurus/src/server/{presets/__tests__/index.test.ts => plugins/__tests__/presets.test.ts} (73%) rename packages/docusaurus/src/server/plugins/__tests__/{applyRouteTrailingSlash.test.ts => routeConfig.test.ts} (71%) delete mode 100644 packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts create mode 100644 packages/docusaurus/src/server/plugins/configs.ts rename packages/docusaurus/src/server/{presets/index.ts => plugins/presets.ts} (95%) create mode 100644 packages/docusaurus/src/server/plugins/routeConfig.ts create mode 100644 packages/docusaurus/src/server/plugins/synthetic.ts rename packages/docusaurus/src/server/{versions/index.ts => siteMetadata.ts} (50%) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 382b0f6365..8093071f16 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -17,7 +17,7 @@ import {loadContext} from '@docusaurus/core/src/server/index'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; import type {RouteConfig} from '@docusaurus/types'; import {posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; -import {sortConfig} from '@docusaurus/core/src/server/plugins'; +import {sortConfig} from '@docusaurus/core/src/server/plugins/routeConfig'; import * as cliDocs from '../cli'; import {validateOptions} from '../options'; diff --git a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts index e33207afc6..6e66d7e4a0 100644 --- a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts @@ -5,10 +5,12 @@ * LICENSE file in the root directory of this source tree. */ +import path from 'path'; import { mergeTranslations, updateTranslationFileMessages, getPluginI18nPath, + localizePath, } from '../i18nUtils'; describe('mergeTranslations', () => { @@ -93,3 +95,85 @@ describe('getPluginI18nPath', () => { ).toMatchInlineSnapshot(`"/i18n/zh-Hans/plugin-content-docs"`); }); }); + +describe('localizePath', () => { + it('localizes url path with current locale', () => { + expect( + localizePath({ + pathType: 'url', + path: '/baseUrl', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + currentLocale: 'fr', + localeConfigs: {}, + }, + options: {localizePath: true}, + }), + ).toBe('/baseUrl/fr/'); + }); + + it('localizes fs path with current locale', () => { + expect( + localizePath({ + pathType: 'fs', + path: '/baseFsPath', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + currentLocale: 'fr', + localeConfigs: {}, + }, + options: {localizePath: true}, + }), + ).toBe(`${path.sep}baseFsPath${path.sep}fr`); + }); + + it('localizes path for default locale, if requested', () => { + expect( + localizePath({ + pathType: 'url', + path: '/baseUrl/', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + currentLocale: 'en', + localeConfigs: {}, + }, + options: {localizePath: true}, + }), + ).toBe('/baseUrl/en/'); + }); + + it('does not localize path for default locale by default', () => { + expect( + localizePath({ + pathType: 'url', + path: '/baseUrl/', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + currentLocale: 'en', + localeConfigs: {}, + }, + // options: {localizePath: true}, + }), + ).toBe('/baseUrl/'); + }); + + it('localizes path for non-default locale by default', () => { + expect( + localizePath({ + pathType: 'url', + path: '/baseUrl/', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + currentLocale: 'en', + localeConfigs: {}, + }, + // options: {localizePath: true}, + }), + ).toBe('/baseUrl/'); + }); +}); diff --git a/packages/docusaurus-utils/src/i18nUtils.ts b/packages/docusaurus-utils/src/i18nUtils.ts index 83bec7163f..950f3719a7 100644 --- a/packages/docusaurus-utils/src/i18nUtils.ts +++ b/packages/docusaurus-utils/src/i18nUtils.ts @@ -7,8 +7,13 @@ import path from 'path'; import _ from 'lodash'; -import type {TranslationFileContent, TranslationFile} from '@docusaurus/types'; +import type { + TranslationFileContent, + TranslationFile, + I18n, +} from '@docusaurus/types'; import {DEFAULT_PLUGIN_ID, I18N_DIR_NAME} from './constants'; +import {normalizeUrl} from './urlUtils'; /** * Takes a list of translation file contents, and shallow-merges them into one. @@ -65,3 +70,46 @@ export function getPluginI18nPath({ ...subPaths, ); } + +/** + * Takes a path and returns a localized a version (which is basically `path + + * i18n.currentLocale`). + */ +export function localizePath({ + pathType, + path: originalPath, + i18n, + options = {}, +}: { + /** + * FS paths will treat Windows specially; URL paths will always have a + * trailing slash to make it a valid base URL. + */ + pathType: 'fs' | 'url'; + /** The path, URL or file path, to be localized. */ + path: string; + /** The current i18n context. */ + i18n: I18n; + options?: { + /** + * By default, we don't localize the path of defaultLocale. This option + * would override that behavior. Setting `false` is useful for `yarn build + * -l zh-Hans` to always emit into the root build directory. + */ + localizePath?: boolean; + }; +}): string { + const shouldLocalizePath: boolean = + // + options.localizePath ?? i18n.currentLocale !== i18n.defaultLocale; + + if (!shouldLocalizePath) { + return originalPath; + } + // FS paths need special care, for Windows support + if (pathType === 'fs') { + return path.join(originalPath, i18n.currentLocale); + } + // Url paths; add a trailing slash so it's a valid base URL + return normalizeUrl([originalPath, i18n.currentLocale, '/']); +} diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index a41197ea06..999cc1928f 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -32,6 +32,7 @@ export { mergeTranslations, updateTranslationFileMessages, getPluginI18nPath, + localizePath, } from './i18nUtils'; export { removeSuffix, diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index e727616533..ef8df767c6 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -28,7 +28,7 @@ import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin'; import {loadI18n} from '../server/i18n'; import {mapAsyncSequential} from '@docusaurus/utils'; -export default async function build( +export async function build( siteDir: string, cliOptions: Partial = {}, // When running build, we force terminate the process to prevent async diff --git a/packages/docusaurus/src/commands/clear.ts b/packages/docusaurus/src/commands/clear.ts index b7e230a06b..600ab07034 100644 --- a/packages/docusaurus/src/commands/clear.ts +++ b/packages/docusaurus/src/commands/clear.ts @@ -26,7 +26,7 @@ async function removePath(entry: {path: string; description: string}) { } } -export default async function clear(siteDir: string): Promise { +export async function clear(siteDir: string): Promise { const generatedFolder = { path: path.join(siteDir, GENERATED_FILES_DIR_NAME), description: 'generated folder', diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index 75a4cd9799..ac7400f349 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -10,7 +10,7 @@ import shell from 'shelljs'; import logger from '@docusaurus/logger'; import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils'; import {loadContext} from '../server'; -import build from './build'; +import {build} from './build'; import type {BuildCLIOptions} from '@docusaurus/types'; import path from 'path'; import os from 'os'; @@ -34,7 +34,7 @@ function shellExecLog(cmd: string) { } } -export default async function deploy( +export async function deploy( siteDir: string, cliOptions: Partial = {}, ): Promise { diff --git a/packages/docusaurus/src/commands/external.ts b/packages/docusaurus/src/commands/external.ts index 799ac9e4c6..52d06b6f33 100644 --- a/packages/docusaurus/src/commands/external.ts +++ b/packages/docusaurus/src/commands/external.ts @@ -6,16 +6,15 @@ */ import type {CommanderStatic} from 'commander'; -import {loadContext, loadPluginConfigs} from '../server'; -import initPlugins from '../server/plugins/init'; +import {loadContext} from '../server'; +import {initPlugins} from '../server/plugins/init'; -export default async function externalCommand( +export async function externalCommand( cli: CommanderStatic, siteDir: string, ): Promise { const context = await loadContext(siteDir); - const pluginConfigs = await loadPluginConfigs(context); - const plugins = await initPlugins({pluginConfigs, context}); + const plugins = await initPlugins(context); // Plugin Lifecycle - extendCli. plugins.forEach((plugin) => { diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index 80d3c51a25..039e477422 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -10,11 +10,11 @@ import serveHandler from 'serve-handler'; import logger from '@docusaurus/logger'; import path from 'path'; import {loadSiteConfig} from '../server'; -import build from './build'; +import {build} from './build'; import {getCLIOptionHost, getCLIOptionPort} from './commandUtils'; import type {ServeCLIOptions} from '@docusaurus/types'; -export default async function serve( +export async function serve( siteDir: string, cliOptions: ServeCLIOptions, ): Promise { diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index bf3e37e26a..81f2314c91 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -28,7 +28,7 @@ import { import {getCLIOptionHost, getCLIOptionPort} from './commandUtils'; import {getTranslationsLocaleDirPath} from '../server/translations/translations'; -export default async function start( +export async function start( siteDir: string, cliOptions: Partial, ): Promise { diff --git a/packages/docusaurus/src/commands/swizzle/__tests__/index.test.ts b/packages/docusaurus/src/commands/swizzle/__tests__/index.test.ts index 3e45151411..7faa5c74c9 100644 --- a/packages/docusaurus/src/commands/swizzle/__tests__/index.test.ts +++ b/packages/docusaurus/src/commands/swizzle/__tests__/index.test.ts @@ -10,7 +10,7 @@ import path from 'path'; import fs from 'fs-extra'; import {ThemePath, createTempSiteDir, Components} from './testUtils'; import tree from 'tree-node-cli'; -import swizzle from '../index'; +import {swizzle} from '../index'; import {escapePath, Globby, posixPath} from '@docusaurus/utils'; const FixtureThemeName = 'fixture-theme-name'; diff --git a/packages/docusaurus/src/commands/swizzle/context.ts b/packages/docusaurus/src/commands/swizzle/context.ts index 7c06ef00c7..d14bce6407 100644 --- a/packages/docusaurus/src/commands/swizzle/context.ts +++ b/packages/docusaurus/src/commands/swizzle/context.ts @@ -5,21 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import {loadContext, loadPluginConfigs} from '../../server'; -import initPlugins, {normalizePluginConfigs} from '../../server/plugins/init'; -import type {InitializedPlugin} from '@docusaurus/types'; +import {loadContext} from '../../server'; +import {initPlugins, normalizePluginConfigs} from '../../server/plugins/init'; +import {loadPluginConfigs} from '../../server/plugins/configs'; import type {SwizzleContext} from './common'; export async function initSwizzleContext( siteDir: string, ): Promise { const context = await loadContext(siteDir); - + const plugins = await initPlugins(context); const pluginConfigs = await loadPluginConfigs(context); - const plugins: InitializedPlugin[] = await initPlugins({ - pluginConfigs, - context, - }); const pluginsNormalized = await normalizePluginConfigs( pluginConfigs, diff --git a/packages/docusaurus/src/commands/swizzle/index.ts b/packages/docusaurus/src/commands/swizzle/index.ts index 604b7266c9..1937785328 100644 --- a/packages/docusaurus/src/commands/swizzle/index.ts +++ b/packages/docusaurus/src/commands/swizzle/index.ts @@ -86,7 +86,7 @@ If you want to swizzle it, use the code=${'--danger'} flag, or confirm that you return undefined; } -export default async function swizzle( +export async function swizzle( siteDir: string, themeNameParam: string | undefined, componentNameParam: string | undefined, diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts index 7c8aed4d3c..1fa8ca558b 100644 --- a/packages/docusaurus/src/commands/writeHeadingIds.ts +++ b/packages/docusaurus/src/commands/writeHeadingIds.ts @@ -11,8 +11,8 @@ import { writeMarkdownHeadingId, type WriteHeadingIDOptions, } from '@docusaurus/utils'; -import {loadContext, loadPluginConfigs} from '../server'; -import initPlugins from '../server/plugins/init'; +import {loadContext} from '../server'; +import {initPlugins} from '../server/plugins/init'; import {safeGlobby} from '../server/utils'; async function transformMarkdownFile( @@ -36,15 +36,11 @@ async function transformMarkdownFile( */ async function getPathsToWatch(siteDir: string): Promise { const context = await loadContext(siteDir); - const pluginConfigs = await loadPluginConfigs(context); - const plugins = await initPlugins({ - pluginConfigs, - context, - }); + const plugins = await initPlugins(context); return plugins.flatMap((plugin) => plugin?.getPathsToWatch?.() ?? []); } -export default async function writeHeadingIds( +export async function writeHeadingIds( siteDir: string, files?: string[], options?: WriteHeadingIDOptions, diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts index 2959d4527f..2c2ddd5b87 100644 --- a/packages/docusaurus/src/commands/writeTranslations.ts +++ b/packages/docusaurus/src/commands/writeTranslations.ts @@ -7,8 +7,8 @@ import type {ConfigOptions, InitializedPlugin} from '@docusaurus/types'; import path from 'path'; -import {loadContext, loadPluginConfigs} from '../server'; -import initPlugins from '../server/plugins/init'; +import {loadContext} from '../server'; +import {initPlugins} from '../server/plugins/init'; import { writePluginTranslations, @@ -72,7 +72,7 @@ async function writePluginTranslationFiles({ } } -export default async function writeTranslations( +export async function writeTranslations( siteDir: string, options: WriteTranslationsOptions & ConfigOptions & {locale?: string}, ): Promise { @@ -80,11 +80,7 @@ export default async function writeTranslations( customConfigFilePath: options.config, locale: options.locale, }); - const pluginConfigs = await loadPluginConfigs(context); - const plugins = await initPlugins({ - pluginConfigs, - context, - }); + const plugins = await initPlugins(context); const locale = options.locale ?? context.i18n.defaultLocale; diff --git a/packages/docusaurus/src/index.ts b/packages/docusaurus/src/index.ts index 3e08a40c4e..097eaad0e0 100644 --- a/packages/docusaurus/src/index.ts +++ b/packages/docusaurus/src/index.ts @@ -5,24 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import build from './commands/build'; -import clear from './commands/clear'; -import deploy from './commands/deploy'; -import externalCommand from './commands/external'; -import serve from './commands/serve'; -import start from './commands/start'; -import swizzle from './commands/swizzle'; -import writeHeadingIds from './commands/writeHeadingIds'; -import writeTranslations from './commands/writeTranslations'; - -export { - build, - clear, - deploy, - externalCommand, - serve, - start, - swizzle, - writeHeadingIds, - writeTranslations, -}; +export {build} from './commands/build'; +export {clear} from './commands/clear'; +export {deploy} from './commands/deploy'; +export {externalCommand} from './commands/external'; +export {serve} from './commands/serve'; +export {start} from './commands/start'; +export {swizzle} from './commands/swizzle'; +export {writeHeadingIds} from './commands/writeHeadingIds'; +export {writeTranslations} from './commands/writeTranslations'; diff --git a/packages/docusaurus/src/server/versions/__tests__/__fixtures__/dummy-plugin.js b/packages/docusaurus/src/server/__tests__/__fixtures__/siteMetadata/dummy-plugin.js similarity index 100% rename from packages/docusaurus/src/server/versions/__tests__/__fixtures__/dummy-plugin.js rename to packages/docusaurus/src/server/__tests__/__fixtures__/siteMetadata/dummy-plugin.js diff --git a/packages/docusaurus/src/server/versions/__tests__/__fixtures__/package.json b/packages/docusaurus/src/server/__tests__/__fixtures__/siteMetadata/package.json similarity index 100% rename from packages/docusaurus/src/server/versions/__tests__/__fixtures__/package.json rename to packages/docusaurus/src/server/__tests__/__fixtures__/siteMetadata/package.json diff --git a/packages/docusaurus/src/server/__tests__/clientModules.test.ts b/packages/docusaurus/src/server/__tests__/clientModules.test.ts new file mode 100644 index 0000000000..c27ba51c19 --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/clientModules.test.ts @@ -0,0 +1,107 @@ +/** + * 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 {loadClientModules} from '../clientModules'; +import type {LoadedPlugin} from '@docusaurus/types'; + +const pluginEmpty: LoadedPlugin = { + name: 'plugin-empty', + path: __dirname, +}; + +const pluginFooBar: LoadedPlugin = { + name: 'plugin-foo-bar', + path: __dirname, + getClientModules() { + return ['foo', 'bar']; + }, +}; + +const pluginHelloWorld: LoadedPlugin = { + plugin: 'plugin-hello-world', + path: __dirname, + getClientModules() { + return [ + // Absolute path + '/hello', + 'world', + ]; + }, +}; + +describe('loadClientModules', () => { + it('loads an empty plugin', () => { + const clientModules = loadClientModules([pluginEmpty]); + expect(clientModules).toMatchInlineSnapshot(`[]`); + }); + + it('loads a non-empty plugin', () => { + const clientModules = loadClientModules([pluginFooBar]); + expect(clientModules).toMatchInlineSnapshot(` + [ + "/packages/docusaurus/src/server/__tests__/foo", + "/packages/docusaurus/src/server/__tests__/bar", + ] + `); + }); + + it('loads multiple non-empty plugins', () => { + const clientModules = loadClientModules([pluginFooBar, pluginHelloWorld]); + expect(clientModules).toMatchInlineSnapshot(` + [ + "/packages/docusaurus/src/server/__tests__/foo", + "/packages/docusaurus/src/server/__tests__/bar", + "/hello", + "/packages/docusaurus/src/server/__tests__/world", + ] + `); + }); + + it('loads multiple non-empty plugins in different order', () => { + const clientModules = loadClientModules([pluginHelloWorld, pluginFooBar]); + expect(clientModules).toMatchInlineSnapshot(` + [ + "/hello", + "/packages/docusaurus/src/server/__tests__/world", + "/packages/docusaurus/src/server/__tests__/foo", + "/packages/docusaurus/src/server/__tests__/bar", + ] + `); + }); + + it('loads both empty and non-empty plugins', () => { + const clientModules = loadClientModules([ + pluginHelloWorld, + pluginEmpty, + pluginFooBar, + ]); + expect(clientModules).toMatchInlineSnapshot(` + [ + "/hello", + "/packages/docusaurus/src/server/__tests__/world", + "/packages/docusaurus/src/server/__tests__/foo", + "/packages/docusaurus/src/server/__tests__/bar", + ] + `); + }); + + it('loads empty and non-empty in a different order', () => { + const clientModules = loadClientModules([ + pluginHelloWorld, + pluginFooBar, + pluginEmpty, + ]); + expect(clientModules).toMatchInlineSnapshot(` + [ + "/hello", + "/packages/docusaurus/src/server/__tests__/world", + "/packages/docusaurus/src/server/__tests__/foo", + "/packages/docusaurus/src/server/__tests__/bar", + ] + `); + }); +}); diff --git a/packages/docusaurus/src/server/__tests__/config.test.ts b/packages/docusaurus/src/server/__tests__/config.test.ts index 0b32cca4d1..71363931e6 100644 --- a/packages/docusaurus/src/server/__tests__/config.test.ts +++ b/packages/docusaurus/src/server/__tests__/config.test.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import loadConfig from '../config'; +import {loadConfig} from '../config'; describe('loadConfig', () => { it('website with valid siteConfig', async () => { diff --git a/packages/docusaurus/src/server/__tests__/htmlTags.test.ts b/packages/docusaurus/src/server/__tests__/htmlTags.test.ts new file mode 100644 index 0000000000..6f7172df2a --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/htmlTags.test.ts @@ -0,0 +1,224 @@ +/** + * 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 {loadHtmlTags} from '../htmlTags'; +import type {LoadedPlugin} from '@docusaurus/types'; + +const pluginEmpty: LoadedPlugin = { + name: 'plugin-empty', +}; + +const pluginPreBodyTags: LoadedPlugin = { + name: 'plugin-preBodyTags', + injectHtmlTags() { + return { + preBodyTags: { + tagName: 'script', + attributes: { + type: 'text/javascript', + async: false, + }, + innerHTML: 'window.foo = null;', + }, + }; + }, +}; + +const pluginHeadTags: LoadedPlugin = { + name: 'plugin-headTags-only', + injectHtmlTags() { + return { + headTags: [ + { + tagName: 'link', + attributes: { + rel: 'preconnect', + href: 'www.google-analytics.com', + }, + }, + { + tagName: 'meta', + attributes: { + name: 'generator', + content: 'Docusaurus', + }, + }, + { + tagName: 'script', + attributes: { + type: 'text/javascript', + src: 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js', + async: true, + 'data-options': '{"prop":true}', + }, + }, + ], + }; + }, +}; + +const pluginPostBodyTags: LoadedPlugin = { + name: 'plugin-postBody-tags', + injectHtmlTags() { + return { + postBodyTags: [ + { + tagName: 'div', + innerHTML: 'Test content', + }, + '', + ], + }; + }, +}; + +const pluginMaybeInjectHeadTags: LoadedPlugin = { + name: 'plugin-postBody-tags', + injectHtmlTags() { + return undefined; + }, +}; + +describe('loadHtmlTags', () => { + it('works for an empty plugin', () => { + const htmlTags = loadHtmlTags([pluginEmpty]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": "", + "postBodyTags": "", + "preBodyTags": "", + } + `); + }); + + it('only injects headTags', () => { + const htmlTags = loadHtmlTags([pluginHeadTags]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": " + + ", + "postBodyTags": "", + "preBodyTags": "", + } + `); + }); + + it('only injects preBodyTags', () => { + const htmlTags = loadHtmlTags([pluginPreBodyTags]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": "", + "postBodyTags": "", + "preBodyTags": "", + } + `); + }); + + it('only injects postBodyTags', () => { + const htmlTags = loadHtmlTags([pluginPostBodyTags]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": "", + "postBodyTags": "
Test content
+ ", + "preBodyTags": "", + } + `); + }); + + it('allows multiple plugins that inject different part of html tags', () => { + const htmlTags = loadHtmlTags([ + pluginHeadTags, + pluginPostBodyTags, + pluginPreBodyTags, + ]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": " + + ", + "postBodyTags": "
Test content
+ ", + "preBodyTags": "", + } + `); + }); + + it('allows multiple plugins that might/might not inject html tags', () => { + const htmlTags = loadHtmlTags([ + pluginEmpty, + pluginHeadTags, + pluginPostBodyTags, + pluginMaybeInjectHeadTags, + ]); + expect(htmlTags).toMatchInlineSnapshot(` + { + "headTags": " + + ", + "postBodyTags": "
Test content
+ ", + "preBodyTags": "", + } + `); + }); + it('throws for invalid tag', () => { + expect(() => + loadHtmlTags([ + { + injectHtmlTags() { + return { + headTags: { + tagName: 'endiliey', + attributes: { + this: 'is invalid', + }, + }, + }; + }, + }, + ]), + ).toThrowErrorMatchingInlineSnapshot( + `"Error loading {\\"tagName\\":\\"endiliey\\",\\"attributes\\":{\\"this\\":\\"is invalid\\"}}, \\"endiliey\\" is not a valid HTML tag."`, + ); + }); + + it('throws for invalid tagName', () => { + expect(() => + loadHtmlTags([ + { + injectHtmlTags() { + return { + headTags: { + tagName: true, + }, + }; + }, + }, + ]), + ).toThrowErrorMatchingInlineSnapshot( + `"{\\"tagName\\":true} is not a valid HTML tag object. \\"tagName\\" must be defined as a string."`, + ); + }); + + it('throws for invalid tag object', () => { + expect(() => + loadHtmlTags([ + { + injectHtmlTags() { + return { + headTags: 2, + }; + }, + }, + ]), + ).toThrowErrorMatchingInlineSnapshot( + `"\\"2\\" is not a valid HTML tag object."`, + ); + }); +}); diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index 27fca3f2a8..258adfbe7c 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -6,9 +6,8 @@ */ import {jest} from '@jest/globals'; -import {loadI18n, localizePath, getDefaultLocaleConfig} from '../i18n'; +import {loadI18n, getDefaultLocaleConfig} from '../i18n'; import {DEFAULT_I18N_CONFIG} from '../configValidation'; -import path from 'path'; import type {I18nConfig} from '@docusaurus/types'; function testLocaleConfigsFor(locales: string[]) { @@ -166,85 +165,3 @@ describe('loadI18n', () => { ); }); }); - -describe('localizePath', () => { - it('localizes url path with current locale', () => { - expect( - localizePath({ - pathType: 'url', - path: '/baseUrl', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - currentLocale: 'fr', - localeConfigs: {}, - }, - options: {localizePath: true}, - }), - ).toBe('/baseUrl/fr/'); - }); - - it('localizes fs path with current locale', () => { - expect( - localizePath({ - pathType: 'fs', - path: '/baseFsPath', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - currentLocale: 'fr', - localeConfigs: {}, - }, - options: {localizePath: true}, - }), - ).toBe(`${path.sep}baseFsPath${path.sep}fr`); - }); - - it('localizes path for default locale, if requested', () => { - expect( - localizePath({ - pathType: 'url', - path: '/baseUrl/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - currentLocale: 'en', - localeConfigs: {}, - }, - options: {localizePath: true}, - }), - ).toBe('/baseUrl/en/'); - }); - - it('does not localize path for default locale by default', () => { - expect( - localizePath({ - pathType: 'url', - path: '/baseUrl/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - currentLocale: 'en', - localeConfigs: {}, - }, - // options: {localizePath: true}, - }), - ).toBe('/baseUrl/'); - }); - - it('localizes path for non-default locale by default', () => { - expect( - localizePath({ - pathType: 'url', - path: '/baseUrl/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - currentLocale: 'en', - localeConfigs: {}, - }, - // options: {localizePath: true}, - }), - ).toBe('/baseUrl/'); - }); -}); diff --git a/packages/docusaurus/src/server/__tests__/routes.test.ts b/packages/docusaurus/src/server/__tests__/routes.test.ts index 70d0309819..662ab03f97 100644 --- a/packages/docusaurus/src/server/__tests__/routes.test.ts +++ b/packages/docusaurus/src/server/__tests__/routes.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import loadRoutes from '../routes'; +import {loadRoutes} from '../routes'; import type {RouteConfig} from '@docusaurus/types'; describe('loadRoutes', () => { diff --git a/packages/docusaurus/src/server/versions/__tests__/index.test.ts b/packages/docusaurus/src/server/__tests__/siteMetadata.test.ts similarity index 73% rename from packages/docusaurus/src/server/versions/__tests__/index.test.ts rename to packages/docusaurus/src/server/__tests__/siteMetadata.test.ts index bf8e15db6c..b12e0a7e17 100644 --- a/packages/docusaurus/src/server/versions/__tests__/index.test.ts +++ b/packages/docusaurus/src/server/__tests__/siteMetadata.test.ts @@ -5,14 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {getPluginVersion} from '..'; +import {getPluginVersion} from '../siteMetadata'; import path from 'path'; describe('getPluginVersion', () => { it('detects external packages plugins versions', async () => { await expect( getPluginVersion( - path.join(__dirname, '__fixtures__/dummy-plugin.js'), + path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'), // Make the plugin appear external. path.join(__dirname, '..', '..', '..', '..', '..', '..', 'website'), ), @@ -22,14 +22,14 @@ describe('getPluginVersion', () => { it('detects project plugins versions', async () => { await expect( getPluginVersion( - path.join(__dirname, '__fixtures__/dummy-plugin.js'), + path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'), // Make the plugin appear project local. - path.join(__dirname, '__fixtures__'), + path.join(__dirname, '__fixtures__/siteMetadata'), ), ).resolves.toEqual({type: 'project'}); }); - it('detect local packages versions', async () => { + it('detects local packages versions', async () => { await expect(getPluginVersion('/', '/')).resolves.toEqual({type: 'local'}); }); }); diff --git a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-empty.js b/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-empty.js deleted file mode 100644 index def0cfe25d..0000000000 --- a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-empty.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-empty', - path: __dirname, - }; -}; diff --git a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-foo-bar.js b/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-foo-bar.js deleted file mode 100644 index 9427657005..0000000000 --- a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-foo-bar.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-foo-bar', - path: __dirname, - getClientModules() { - return ['foo', 'bar']; - }, - }; -}; diff --git a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-hello-world.js b/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-hello-world.js deleted file mode 100644 index 3047719c6d..0000000000 --- a/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/plugin-hello-world.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 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 = function() { - return { - plugin: 'plugin-hello-world', - path: __dirname, - getClientModules() { - return ['hello', 'world']; - }, - }; -}; diff --git a/packages/docusaurus/src/server/client-modules/__tests__/index.test.ts b/packages/docusaurus/src/server/client-modules/__tests__/index.test.ts deleted file mode 100644 index 1eb43c28b9..0000000000 --- a/packages/docusaurus/src/server/client-modules/__tests__/index.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * 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 loadClientModules from '../index'; - -import pluginEmpty from './__fixtures__/plugin-empty'; -import pluginFooBar from './__fixtures__/plugin-foo-bar'; -import pluginHelloWorld from './__fixtures__/plugin-hello-world'; - -describe('loadClientModules', () => { - it('empty', () => { - const clientModules = loadClientModules([pluginEmpty()]); - expect(clientModules).toMatchInlineSnapshot(`[]`); - }); - - it('non-empty', () => { - const clientModules = loadClientModules([pluginFooBar()]); - expect(clientModules).toMatchInlineSnapshot(` - [ - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar", - ] - `); - }); - - it('multiple non-empty', () => { - const clientModules = loadClientModules([ - pluginFooBar(), - pluginHelloWorld(), - ]); - expect(clientModules).toMatchInlineSnapshot(` - [ - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world", - ] - `); - }); - - it('multiple non-empty different order', () => { - const clientModules = loadClientModules([ - pluginHelloWorld(), - pluginFooBar(), - ]); - expect(clientModules).toMatchInlineSnapshot(` - [ - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar", - ] - `); - }); - - it('empty and non-empty', () => { - const clientModules = loadClientModules([ - pluginHelloWorld(), - pluginEmpty(), - pluginFooBar(), - ]); - expect(clientModules).toMatchInlineSnapshot(` - [ - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar", - ] - `); - }); - - it('empty and non-empty different order', () => { - const clientModules = loadClientModules([ - pluginHelloWorld(), - pluginFooBar(), - pluginEmpty(), - ]); - expect(clientModules).toMatchInlineSnapshot(` - [ - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo", - "/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar", - ] - `); - }); -}); diff --git a/packages/docusaurus/src/server/client-modules/index.ts b/packages/docusaurus/src/server/clientModules.ts similarity index 81% rename from packages/docusaurus/src/server/client-modules/index.ts rename to packages/docusaurus/src/server/clientModules.ts index 6a51d751a5..697e4c2528 100644 --- a/packages/docusaurus/src/server/client-modules/index.ts +++ b/packages/docusaurus/src/server/clientModules.ts @@ -8,9 +8,7 @@ import path from 'path'; import type {LoadedPlugin} from '@docusaurus/types'; -export default function loadClientModules( - plugins: LoadedPlugin[], -): string[] { +export function loadClientModules(plugins: LoadedPlugin[]): string[] { return plugins.flatMap( (plugin) => plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ?? diff --git a/packages/docusaurus/src/server/config.ts b/packages/docusaurus/src/server/config.ts index 94e8656acf..05b3ad2e8b 100644 --- a/packages/docusaurus/src/server/config.ts +++ b/packages/docusaurus/src/server/config.ts @@ -10,7 +10,7 @@ import importFresh from 'import-fresh'; import type {DocusaurusConfig} from '@docusaurus/types'; import {validateConfig} from './configValidation'; -export default async function loadConfig( +export async function loadConfig( configPath: string, ): Promise { if (!(await fs.pathExists(configPath))) { diff --git a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-empty.js b/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-empty.js deleted file mode 100644 index 6b8398c7d4..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-empty.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-empty', - }; -}; diff --git a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-headTags.js b/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-headTags.js deleted file mode 100644 index 99b0d07b9a..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-headTags.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-headTags-only', - injectHtmlTags() { - return { - headTags: [ - { - tagName: 'link', - attributes: { - rel: 'preconnect', - href: 'www.google-analytics.com', - }, - }, - ``, - ], - }; - }, - }; -}; diff --git a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-postBodyTags.js b/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-postBodyTags.js deleted file mode 100644 index 6ce0398aed..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-postBodyTags.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-postBody-tags', - injectHtmlTags() { - return { - postBodyTags: [ - { - tagName: 'div', - innerHTML: 'Test content', - }, - ], - }; - }, - }; -}; diff --git a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-preBodyTags.js b/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-preBodyTags.js deleted file mode 100644 index db23f8c883..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/__fixtures__/plugin-preBodyTags.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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 = function() { - return { - name: 'plugin-preBodyTags', - injectHtmlTags() { - return { - preBodyTags: { - tagName: 'script', - attributes: { - type: 'text/javascript', - }, - innerHTML: 'window.foo = null;', - }, - }; - }, - }; -}; diff --git a/packages/docusaurus/src/server/html-tags/__tests__/htmlTags.test.ts b/packages/docusaurus/src/server/html-tags/__tests__/htmlTags.test.ts deleted file mode 100644 index 90d547952a..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/htmlTags.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * 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 htmlTagObjectToString from '../htmlTags'; - -describe('htmlTagObjectToString', () => { - it('valid html tag', () => { - expect( - htmlTagObjectToString({ - tagName: 'script', - attributes: { - type: 'text/javascript', - src: 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js', - async: true, - 'data-options': '{"prop":true}', - }, - }), - ).toMatchInlineSnapshot( - `""`, - ); - - expect( - htmlTagObjectToString({ - tagName: 'link', - attributes: { - rel: 'preconnect', - href: 'www.google-analytics.com', - }, - }), - ).toMatchInlineSnapshot( - `""`, - ); - - expect( - htmlTagObjectToString({ - tagName: 'div', - attributes: { - style: 'background-color:lightblue', - }, - innerHTML: 'Lightblue color here', - }), - ).toMatchInlineSnapshot( - `"
Lightblue color here
"`, - ); - - expect( - htmlTagObjectToString({ - tagName: 'div', - innerHTML: 'Test', - }), - ).toMatchInlineSnapshot(`"
Test
"`); - }); - - it('valid html void tag', () => { - expect( - htmlTagObjectToString({ - tagName: 'meta', - attributes: { - name: 'generator', - content: 'Docusaurus', - }, - }), - ).toMatchInlineSnapshot( - `""`, - ); - - expect( - htmlTagObjectToString({ - tagName: 'img', - attributes: { - src: '/img/docusaurus.png', - alt: 'Docusaurus logo', - height: '42', - width: '42', - }, - }), - ).toMatchInlineSnapshot( - `"\\"Docusaurus"`, - ); - }); - - it('invalid tag', () => { - expect(() => - htmlTagObjectToString({ - tagName: 'endiliey', - attributes: { - this: 'is invalid', - }, - }), - ).toThrowErrorMatchingInlineSnapshot( - `"Error loading {\\"tagName\\":\\"endiliey\\",\\"attributes\\":{\\"this\\":\\"is invalid\\"}}, \\"endiliey\\" is not a valid HTML tags."`, - ); - }); - - it('invalid tagName', () => { - expect(() => - htmlTagObjectToString({ - tagName: true, - }), - ).toThrowErrorMatchingInlineSnapshot( - `"{\\"tagName\\":true} is not a valid HTML tag object. \\"tagName\\" must be defined as a string."`, - ); - }); - - it('invalid html tag object', () => { - expect(() => - htmlTagObjectToString('foo'), - ).toThrowErrorMatchingInlineSnapshot( - `"\\"foo\\" is not a valid HTML tag object."`, - ); - - expect(() => - htmlTagObjectToString(null), - ).toThrowErrorMatchingInlineSnapshot( - `"\\"null\\" is not a valid HTML tag object."`, - ); - }); -}); diff --git a/packages/docusaurus/src/server/html-tags/__tests__/index.test.ts b/packages/docusaurus/src/server/html-tags/__tests__/index.test.ts deleted file mode 100644 index 19786fbea4..0000000000 --- a/packages/docusaurus/src/server/html-tags/__tests__/index.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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 {loadHtmlTags} from '../index'; - -import pluginEmpty from './__fixtures__/plugin-empty'; -import pluginPreBodyTags from './__fixtures__/plugin-preBodyTags'; -import pluginHeadTags from './__fixtures__/plugin-headTags'; -import pluginPostBodyTags from './__fixtures__/plugin-postBodyTags'; - -describe('loadHtmlTags', () => { - it('empty plugin', () => { - const htmlTags = loadHtmlTags([pluginEmpty()]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": "", - "postBodyTags": "", - "preBodyTags": "", - } - `); - }); - - it('only inject headTags', () => { - const htmlTags = loadHtmlTags([pluginHeadTags()]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": " - ", - "postBodyTags": "", - "preBodyTags": "", - } - `); - }); - - it('only inject preBodyTags', () => { - const htmlTags = loadHtmlTags([pluginPreBodyTags()]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": "", - "postBodyTags": "", - "preBodyTags": "", - } - `); - }); - - it('only inject postBodyTags', () => { - const htmlTags = loadHtmlTags([pluginPostBodyTags()]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": "", - "postBodyTags": "
Test content
", - "preBodyTags": "", - } - `); - }); - - it('multiple plugins that inject different part of html tags', () => { - const htmlTags = loadHtmlTags([ - pluginHeadTags(), - pluginPostBodyTags(), - pluginPreBodyTags(), - ]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": " - ", - "postBodyTags": "
Test content
", - "preBodyTags": "", - } - `); - }); - - it('multiple plugins that might/might not inject html tags', () => { - const htmlTags = loadHtmlTags([ - pluginEmpty(), - pluginHeadTags(), - pluginPostBodyTags(), - ]); - expect(htmlTags).toMatchInlineSnapshot(` - { - "headTags": " - ", - "postBodyTags": "
Test content
", - "preBodyTags": "", - } - `); - }); -}); diff --git a/packages/docusaurus/src/server/html-tags/htmlTags.ts b/packages/docusaurus/src/server/html-tags/htmlTags.ts deleted file mode 100644 index 83946c1978..0000000000 --- a/packages/docusaurus/src/server/html-tags/htmlTags.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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 type {HtmlTagObject} from '@docusaurus/types'; -import htmlTags from 'html-tags'; -import voidHtmlTags from 'html-tags/void'; -import escapeHTML from 'escape-html'; - -function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject { - if (typeof val !== 'object' || !val) { - throw new Error(`"${val}" is not a valid HTML tag object.`); - } - if (typeof (val as HtmlTagObject).tagName !== 'string') { - throw new Error( - `${JSON.stringify( - val, - )} is not a valid HTML tag object. "tagName" must be defined as a string.`, - ); - } -} - -export default function htmlTagObjectToString(tagDefinition: unknown): string { - assertIsHtmlTagObject(tagDefinition); - if (htmlTags.indexOf(tagDefinition.tagName) === -1) { - throw new Error( - `Error loading ${JSON.stringify(tagDefinition)}, "${ - tagDefinition.tagName - }" is not a valid HTML tags.`, - ); - } - const isVoidTag = voidHtmlTags.indexOf(tagDefinition.tagName) !== -1; - const tagAttributes = tagDefinition.attributes ?? {}; - const attributes = Object.keys(tagAttributes) - .filter((attributeName) => tagAttributes[attributeName] !== false) - .map((attributeName) => { - if (tagAttributes[attributeName] === true) { - return attributeName; - } - return `${attributeName}="${escapeHTML( - tagAttributes[attributeName] as string, - )}"`; - }); - return `<${[tagDefinition.tagName].concat(attributes).join(' ')}>${ - (!isVoidTag && tagDefinition.innerHTML) || '' - }${isVoidTag ? '' : ``}`; -} diff --git a/packages/docusaurus/src/server/html-tags/index.ts b/packages/docusaurus/src/server/html-tags/index.ts deleted file mode 100644 index c32d008e12..0000000000 --- a/packages/docusaurus/src/server/html-tags/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 htmlTagObjectToString from './htmlTags'; -import type { - InjectedHtmlTags, - HtmlTagObject, - HtmlTags, - LoadedPlugin, -} from '@docusaurus/types'; - -function toString(val: string | HtmlTagObject): string { - return typeof val === 'string' ? val : htmlTagObjectToString(val); -} - -function createHtmlTagsString(tags: HtmlTags): string { - return Array.isArray(tags) ? tags.map(toString).join('\n') : toString(tags); -} - -export function loadHtmlTags(plugins: LoadedPlugin[]): InjectedHtmlTags { - const htmlTags = plugins.reduce( - (acc, plugin) => { - if (!plugin.injectHtmlTags) { - return acc; - } - const {headTags, preBodyTags, postBodyTags} = - plugin.injectHtmlTags({content: plugin.content}) ?? {}; - return { - headTags: headTags - ? `${acc.headTags}\n${createHtmlTagsString(headTags)}` - : acc.headTags, - preBodyTags: preBodyTags - ? `${acc.preBodyTags}\n${createHtmlTagsString(preBodyTags)}` - : acc.preBodyTags, - postBodyTags: postBodyTags - ? `${acc.postBodyTags}\n${createHtmlTagsString(postBodyTags)}` - : acc.postBodyTags, - }; - }, - {headTags: '', preBodyTags: '', postBodyTags: ''}, - ); - - return { - headTags: htmlTags.headTags.trim(), - preBodyTags: htmlTags.preBodyTags.trim(), - postBodyTags: htmlTags.postBodyTags.trim(), - }; -} diff --git a/packages/docusaurus/src/server/htmlTags.ts b/packages/docusaurus/src/server/htmlTags.ts new file mode 100644 index 0000000000..28e04cec80 --- /dev/null +++ b/packages/docusaurus/src/server/htmlTags.ts @@ -0,0 +1,81 @@ +/** + * 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 htmlTags from 'html-tags'; +import voidHtmlTags from 'html-tags/void'; +import escapeHTML from 'escape-html'; +import _ from 'lodash'; +import type { + InjectedHtmlTags, + HtmlTagObject, + HtmlTags, + LoadedPlugin, +} from '@docusaurus/types'; + +function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject { + if (typeof val !== 'object' || !val) { + throw new Error(`"${val}" is not a valid HTML tag object.`); + } + if (typeof (val as HtmlTagObject).tagName !== 'string') { + throw new Error( + `${JSON.stringify( + val, + )} is not a valid HTML tag object. "tagName" must be defined as a string.`, + ); + } + if (!htmlTags.includes((val as HtmlTagObject).tagName)) { + throw new Error( + `Error loading ${JSON.stringify(val)}, "${ + (val as HtmlTagObject).tagName + }" is not a valid HTML tag.`, + ); + } +} + +function htmlTagObjectToString(tag: unknown): string { + assertIsHtmlTagObject(tag); + const isVoidTag = voidHtmlTags.includes(tag.tagName); + const tagAttributes = tag.attributes ?? {}; + const attributes = Object.keys(tagAttributes) + .map((attr) => { + const value = tagAttributes[attr]!; + if (typeof value === 'boolean') { + return value ? attr : undefined; + } + return `${attr}="${escapeHTML(value)}"`; + }) + .filter((str): str is string => Boolean(str)); + const openingTag = `<${[tag.tagName].concat(attributes).join(' ')}>`; + const innerHTML = (!isVoidTag && tag.innerHTML) || ''; + const closingTag = isVoidTag ? '' : ``; + return openingTag + innerHTML + closingTag; +} + +function createHtmlTagsString(tags: HtmlTags | undefined): string { + return (Array.isArray(tags) ? tags : [tags]) + .filter(Boolean) + .map((val) => (typeof val === 'string' ? val : htmlTagObjectToString(val))) + .join('\n'); +} + +export function loadHtmlTags(plugins: LoadedPlugin[]): InjectedHtmlTags { + const pluginHtmlTags = plugins.map( + (plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {}, + ); + const tagTypes = ['headTags', 'preBodyTags', 'postBodyTags'] as const; + return Object.fromEntries( + _.zip( + tagTypes, + tagTypes.map((type) => + pluginHtmlTags + .map((tags) => createHtmlTagsString(tags[type])) + .join('\n') + .trim(), + ), + ), + ); +} diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 713a347459..42b363be7d 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -5,11 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; -import path from 'path'; -import {normalizeUrl} from '@docusaurus/utils'; import {getLangDir} from 'rtl-detect'; import logger from '@docusaurus/logger'; +import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; function getDefaultLocaleLabel(locale: string) { const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of( @@ -64,29 +62,3 @@ Note: Docusaurus only support running one locale at a time.`; localeConfigs, }; } - -export function localizePath({ - pathType, - path: originalPath, - i18n, - options = {}, -}: { - pathType: 'fs' | 'url'; - path: string; - i18n: I18n; - options?: {localizePath?: boolean}; -}): string { - const shouldLocalizePath: boolean = - // By default, we don't localize the path of defaultLocale - options.localizePath ?? i18n.currentLocale !== i18n.defaultLocale; - - if (!shouldLocalizePath) { - return originalPath; - } - // FS paths need special care, for Windows support - if (pathType === 'fs') { - return path.join(originalPath, i18n.currentLocale); - } - // Url paths; add a trailing slash so it's a valid base URL - return normalizeUrl([originalPath, i18n.currentLocale, '/']); -} diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index ae4c371c1f..fc4c3afbea 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -8,40 +8,27 @@ import { generate, escapePath, + localizePath, DEFAULT_BUILD_DIR_NAME, DEFAULT_CONFIG_FILE_NAME, GENERATED_FILES_DIR_NAME, } from '@docusaurus/utils'; +import _ from 'lodash'; import path from 'path'; -import logger from '@docusaurus/logger'; import ssrDefaultTemplate from '../webpack/templates/ssr.html.template'; -import loadClientModules from './client-modules'; -import loadConfig from './config'; +import {loadClientModules} from './clientModules'; +import {loadConfig} from './config'; import {loadPlugins} from './plugins'; -import loadPresets from './presets'; -import loadRoutes from './routes'; -import type { - DocusaurusConfig, - DocusaurusSiteMetadata, - HtmlTagObject, - LoadContext, - LoadedPlugin, - PluginConfig, - Props, -} from '@docusaurus/types'; -import {loadHtmlTags} from './html-tags'; -import {getPackageJsonVersion} from './versions'; +import {loadRoutes} from './routes'; +import {loadHtmlTags} from './htmlTags'; +import {loadSiteMetadata} from './siteMetadata'; import {handleDuplicateRoutes} from './duplicateRoutes'; -import {loadI18n, localizePath} from './i18n'; +import {loadI18n} from './i18n'; import { readCodeTranslationFileContent, getPluginsDefaultCodeTranslationMessages, } from './translations/translations'; -import _ from 'lodash'; -import type {RuleSetRule} from 'webpack'; -import admonitions from 'remark-admonitions'; -import {createRequire} from 'module'; -import {resolveModuleName} from './moduleShorthand'; +import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types'; export type LoadContextOptions = { customOutDir?: string; @@ -126,171 +113,6 @@ export async function loadContext( }; } -export async function loadPluginConfigs( - context: LoadContext, -): Promise { - let {plugins: presetPlugins, themes: presetThemes} = await loadPresets( - context, - ); - const {siteConfig, siteConfigPath} = context; - const require = createRequire(siteConfigPath); - function normalizeShorthand( - pluginConfig: PluginConfig, - pluginType: 'plugin' | 'theme', - ): PluginConfig { - if (typeof pluginConfig === 'string') { - return resolveModuleName(pluginConfig, require, pluginType); - } else if ( - Array.isArray(pluginConfig) && - typeof pluginConfig[0] === 'string' - ) { - return [ - resolveModuleName(pluginConfig[0], require, pluginType), - pluginConfig[1] ?? {}, - ]; - } - return pluginConfig; - } - presetPlugins = presetPlugins.map((plugin) => - normalizeShorthand(plugin, 'plugin'), - ); - presetThemes = presetThemes.map((theme) => - normalizeShorthand(theme, 'theme'), - ); - const standalonePlugins = siteConfig.plugins.map((plugin) => - normalizeShorthand(plugin, 'plugin'), - ); - const standaloneThemes = siteConfig.themes.map((theme) => - normalizeShorthand(theme, 'theme'), - ); - return [ - ...presetPlugins, - ...presetThemes, - // Site config should be the highest priority. - ...standalonePlugins, - ...standaloneThemes, - ]; -} - -// Make a fake plugin to: -// - Resolve aliased theme components -// - Inject scripts/stylesheets -function createBootstrapPlugin({ - siteDir, - siteConfig, -}: { - siteDir: string; - siteConfig: DocusaurusConfig; -}): LoadedPlugin { - const { - stylesheets, - scripts, - clientModules: siteConfigClientModules, - } = siteConfig; - return { - name: 'docusaurus-bootstrap-plugin', - content: null, - options: { - id: 'default', - }, - version: {type: 'synthetic'}, - path: siteDir, - getClientModules() { - return siteConfigClientModules; - }, - injectHtmlTags: () => { - const stylesheetsTags = stylesheets.map((source) => - typeof source === 'string' - ? `` - : ({ - tagName: 'link', - attributes: { - rel: 'stylesheet', - ...source, - }, - } as HtmlTagObject), - ); - const scriptsTags = scripts.map((source) => - typeof source === 'string' - ? `` - : ({ - tagName: 'script', - attributes: { - ...source, - }, - } as HtmlTagObject), - ); - return { - headTags: [...stylesheetsTags, ...scriptsTags], - }; - }, - }; -} - -/** - * Configure Webpack fallback mdx loader for md/mdx files out of content-plugin - * folders. Adds a "fallback" mdx loader for mdx files that are not processed by - * content plugins. This allows to do things such as importing repo/README.md as - * a partial from another doc. Not ideal solution, but good enough for now - */ -function createMDXFallbackPlugin({ - siteDir, - siteConfig, -}: { - siteDir: string; - siteConfig: DocusaurusConfig; -}): LoadedPlugin { - return { - name: 'docusaurus-mdx-fallback-plugin', - content: null, - options: { - id: 'default', - }, - version: {type: 'synthetic'}, - // Synthetic, the path doesn't matter much - path: '.', - configureWebpack(config, isServer, {getJSLoader}) { - // We need the mdx fallback loader to exclude files that were already - // processed by content plugins mdx loaders. This works, but a bit - // hacky... Not sure there's a way to handle that differently in webpack - function getMDXFallbackExcludedPaths(): string[] { - const rules: RuleSetRule[] = config?.module?.rules as RuleSetRule[]; - return rules.flatMap((rule) => { - const isMDXRule = - rule.test instanceof RegExp && rule.test.test('x.mdx'); - return isMDXRule ? (rule.include as string[]) : []; - }); - } - - return { - module: { - rules: [ - { - test: /\.mdx?$/i, - exclude: getMDXFallbackExcludedPaths(), - use: [ - getJSLoader({isServer}), - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: { - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - siteDir, - isMDXPartial: () => true, // External mdx files are always meant to be imported as partials - isMDXPartialFrontMatterWarningDisabled: true, // External mdx files might have front matter, let's just disable the warning - remarkPlugins: [admonitions], - }, - }, - ], - }, - ], - }, - }; - }, - }; -} - export async function load( siteDir: string, options: LoadContextOptions = {}, @@ -308,9 +130,8 @@ export async function load( codeTranslations, } = context; // Plugins. - const pluginConfigs: PluginConfig[] = await loadPluginConfigs(context); const {plugins, pluginsRouteConfigs, globalData, themeConfigTranslated} = - await loadPlugins({pluginConfigs, context}); + await loadPlugins(context); // Side-effect to replace the untranslated themeConfig by the translated one context.siteConfig.themeConfig = themeConfigTranslated; @@ -341,11 +162,6 @@ export default ${JSON.stringify(siteConfig, null, 2)}; `, ); - plugins.push( - createBootstrapPlugin({siteDir, siteConfig}), - createMDXFallbackPlugin({siteDir, siteConfig}), - ); - // Load client modules. const clientModules = loadClientModules(plugins); const genClientModules = generate( @@ -416,21 +232,7 @@ ${Object.entries(registry) ); // Version metadata. - const siteMetadata: DocusaurusSiteMetadata = { - docusaurusVersion: (await getPackageJsonVersion( - path.join(__dirname, '../../package.json'), - ))!, - siteVersion: await getPackageJsonVersion( - path.join(siteDir, 'package.json'), - ), - pluginVersions: {}, - }; - plugins - .filter(({version: {type}}) => type !== 'synthetic') - .forEach(({name, version}) => { - siteMetadata.pluginVersions[name] = version; - }); - checkDocusaurusPackagesVersion(siteMetadata); + const siteMetadata = await loadSiteMetadata({plugins, siteDir}); const genSiteMetadata = generate( generatedFilesDir, 'site-metadata.json', @@ -471,26 +273,3 @@ ${Object.entries(registry) return props; } - -// We want all @docusaurus/* packages to have the exact same version! -// See https://github.com/facebook/docusaurus/issues/3371 -// See https://github.com/facebook/docusaurus/pull/3386 -function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) { - const {docusaurusVersion} = siteMetadata; - Object.entries(siteMetadata.pluginVersions).forEach( - ([plugin, versionInfo]) => { - if ( - versionInfo.type === 'package' && - versionInfo.name?.startsWith('@docusaurus/') && - versionInfo.version && - versionInfo.version !== docusaurusVersion - ) { - // should we throw instead? - // It still could work with different versions - logger.error`Invalid name=${plugin} version number=${versionInfo.version}. -All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${docusaurusVersion}). -Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`; - } - }, - ); -} diff --git a/packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-mixed.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-mixed.js similarity index 100% rename from packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-mixed.js rename to packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-mixed.js diff --git a/packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-plugins.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-plugins.js similarity index 100% rename from packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-plugins.js rename to packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-plugins.js diff --git a/packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-themes.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-themes.js similarity index 100% rename from packages/docusaurus/src/server/presets/__tests__/__fixtures__/preset-themes.js rename to packages/docusaurus/src/server/plugins/__tests__/__fixtures__/presets/preset-themes.js diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap index 7435ca95c5..0a16be1b45 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap @@ -37,112 +37,33 @@ exports[`loadPlugins loads plugins 1`] = ` "type": "local", }, }, + { + "content": undefined, + "getClientModules": [Function], + "injectHtmlTags": [Function], + "name": "docusaurus-bootstrap-plugin", + "options": { + "id": "default", + }, + "path": "/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin", + "version": { + "type": "synthetic", + }, + }, + { + "configureWebpack": [Function], + "content": undefined, + "name": "docusaurus-mdx-fallback-plugin", + "options": { + "id": "default", + }, + "path": ".", + "version": { + "type": "synthetic", + }, + }, ], "pluginsRouteConfigs": [], "themeConfigTranslated": {}, } `; - -exports[`sortConfig sorts route config correctly 1`] = ` -[ - { - "component": "", - "path": "/community", - }, - { - "component": "", - "path": "/some-page", - }, - { - "component": "", - "path": "/docs", - "routes": [ - { - "component": "", - "path": "/docs/someDoc", - }, - { - "component": "", - "path": "/docs/someOtherDoc", - }, - ], - }, - { - "component": "", - "path": "/", - }, - { - "component": "", - "path": "/", - "routes": [ - { - "component": "", - "path": "/someDoc", - }, - { - "component": "", - "path": "/someOtherDoc", - }, - ], - }, - { - "component": "", - "path": "/", - "routes": [ - { - "component": "", - "path": "/subroute", - }, - ], - }, -] -`; - -exports[`sortConfig sorts route config given a baseURL 1`] = ` -[ - { - "component": "", - "path": "/latest/community", - }, - { - "component": "", - "path": "/latest/example", - }, - { - "component": "", - "path": "/latest/some-page", - }, - { - "component": "", - "path": "/latest/docs", - "routes": [ - { - "component": "", - "path": "/latest/docs/someDoc", - }, - { - "component": "", - "path": "/latest/docs/someOtherDoc", - }, - ], - }, - { - "component": "", - "path": "/latest/", - }, - { - "component": "", - "path": "/latest/", - "routes": [ - { - "component": "", - "path": "/latest/someDoc", - }, - { - "component": "", - "path": "/latest/someOtherDoc", - }, - ], - }, -] -`; diff --git a/packages/docusaurus/src/server/presets/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/presets.test.ts.snap similarity index 100% rename from packages/docusaurus/src/server/presets/__tests__/__snapshots__/index.test.ts.snap rename to packages/docusaurus/src/server/plugins/__tests__/__snapshots__/presets.test.ts.snap diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap new file mode 100644 index 0000000000..e38931b909 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sortConfig sorts route config correctly 1`] = ` +[ + { + "component": "", + "path": "/community", + }, + { + "component": "", + "path": "/some-page", + }, + { + "component": "", + "path": "/docs", + "routes": [ + { + "component": "", + "path": "/docs/someDoc", + }, + { + "component": "", + "path": "/docs/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/", + }, + { + "component": "", + "path": "/", + "routes": [ + { + "component": "", + "path": "/someDoc", + }, + { + "component": "", + "path": "/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/", + "routes": [ + { + "component": "", + "path": "/subroute", + }, + ], + }, +] +`; + +exports[`sortConfig sorts route config given a baseURL 1`] = ` +[ + { + "component": "", + "path": "/latest/community", + }, + { + "component": "", + "path": "/latest/example", + }, + { + "component": "", + "path": "/latest/some-page", + }, + { + "component": "", + "path": "/latest/docs", + "routes": [ + { + "component": "", + "path": "/latest/docs/someDoc", + }, + { + "component": "", + "path": "/latest/docs/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/latest/", + }, + { + "component": "", + "path": "/latest/", + "routes": [ + { + "component": "", + "path": "/latest/someDoc", + }, + { + "component": "", + "path": "/latest/someOtherDoc", + }, + ], + }, +] +`; diff --git a/packages/docusaurus/src/server/plugins/__tests__/index.test.ts b/packages/docusaurus/src/server/plugins/__tests__/index.test.ts index 757c68d71d..d585d644c4 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/index.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/index.test.ts @@ -6,134 +6,46 @@ */ import path from 'path'; -import {loadPlugins, sortConfig} from '..'; -import type {RouteConfig} from '@docusaurus/types'; +import {loadPlugins} from '..'; describe('loadPlugins', () => { it('loads plugins', async () => { const siteDir = path.join(__dirname, '__fixtures__/site-with-plugin'); await expect( loadPlugins({ - pluginConfigs: [ - () => ({ - name: 'test1', - prop: 'a', - async loadContent() { - // Testing that plugin lifecycle is bound to the plugin instance - return this.prop; - }, - async contentLoaded({content, actions}) { - actions.setGlobalData({content, prop: this.prop}); - }, - }), - () => ({ - name: 'test2', - configureWebpack() { - return {}; - }, - }), - ], - - context: { - siteDir, - generatedFilesDir: path.join(siteDir, '.docusaurus'), - outDir: path.join(siteDir, 'build'), - // @ts-expect-error: good enough - siteConfig: { - baseUrl: '/', - trailingSlash: true, - themeConfig: {}, - }, - - siteConfigPath: path.join(siteDir, 'docusaurus.config.js'), + siteDir, + generatedFilesDir: path.join(siteDir, '.docusaurus'), + outDir: path.join(siteDir, 'build'), + // @ts-expect-error: good enough + siteConfig: { + baseUrl: '/', + trailingSlash: true, + themeConfig: {}, + presets: [], + plugins: [ + () => ({ + name: 'test1', + prop: 'a', + async loadContent() { + // Testing that plugin lifecycle is bound to the plugin instance + return this.prop; + }, + async contentLoaded({content, actions}) { + actions.setGlobalData({content, prop: this.prop}); + }, + }), + ], + themes: [ + () => ({ + name: 'test2', + configureWebpack() { + return {}; + }, + }), + ], }, + siteConfigPath: path.join(siteDir, 'docusaurus.config.js'), }), ).resolves.toMatchSnapshot(); }); }); - -describe('sortConfig', () => { - it('sorts route config correctly', () => { - const routes: RouteConfig[] = [ - { - path: '/', - component: '', - routes: [ - {path: '/someDoc', component: ''}, - {path: '/someOtherDoc', component: ''}, - ], - }, - { - path: '/', - component: '', - }, - { - path: '/', - component: '', - routes: [{path: '/subroute', component: ''}], - }, - { - path: '/docs', - component: '', - routes: [ - {path: '/docs/someDoc', component: ''}, - {path: '/docs/someOtherDoc', component: ''}, - ], - }, - { - path: '/community', - component: '', - }, - { - path: '/some-page', - component: '', - }, - ]; - - sortConfig(routes); - - expect(routes).toMatchSnapshot(); - }); - - it('sorts route config given a baseURL', () => { - const baseURL = '/latest/'; - const routes: RouteConfig[] = [ - { - path: baseURL, - component: '', - routes: [ - {path: `${baseURL}someDoc`, component: ''}, - {path: `${baseURL}someOtherDoc`, component: ''}, - ], - }, - { - path: `${baseURL}example`, - component: '', - }, - { - path: `${baseURL}docs`, - component: '', - routes: [ - {path: `${baseURL}docs/someDoc`, component: ''}, - {path: `${baseURL}docs/someOtherDoc`, component: ''}, - ], - }, - { - path: `${baseURL}community`, - component: '', - }, - { - path: `${baseURL}some-page`, - component: '', - }, - { - path: `${baseURL}`, - component: '', - }, - ]; - - sortConfig(routes, baseURL); - - expect(routes).toMatchSnapshot(); - }); -}); diff --git a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts index 87a553cb92..3155775b52 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts @@ -7,22 +7,14 @@ import path from 'path'; -import { - loadContext, - loadPluginConfigs, - type LoadContextOptions, -} from '../../index'; -import initPlugins from '../init'; +import {loadContext, type LoadContextOptions} from '../../index'; +import {initPlugins} from '../init'; describe('initPlugins', () => { async function loadSite(options: LoadContextOptions = {}) { const siteDir = path.join(__dirname, '__fixtures__', 'site-with-plugin'); const context = await loadContext(siteDir, options); - const pluginConfigs = await loadPluginConfigs(context); - const plugins = await initPlugins({ - pluginConfigs, - context, - }); + const plugins = await initPlugins(context); return {siteDir, context, plugins}; } diff --git a/packages/docusaurus/src/server/presets/__tests__/index.test.ts b/packages/docusaurus/src/server/plugins/__tests__/presets.test.ts similarity index 73% rename from packages/docusaurus/src/server/presets/__tests__/index.test.ts rename to packages/docusaurus/src/server/plugins/__tests__/presets.test.ts index c3ce1f8b06..a50117e371 100644 --- a/packages/docusaurus/src/server/presets/__tests__/index.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/presets.test.ts @@ -7,7 +7,7 @@ import path from 'path'; -import loadPresets from '../index'; +import {loadPresets} from '../presets'; import type {LoadContext} from '@docusaurus/types'; describe('loadPresets', () => { @@ -31,7 +31,9 @@ describe('loadPresets', () => { const context = { siteConfigPath: __dirname, siteConfig: { - presets: [path.join(__dirname, '__fixtures__/preset-plugins.js')], + presets: [ + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), + ], }, } as LoadContext; const presets = await loadPresets(context); @@ -43,8 +45,8 @@ describe('loadPresets', () => { siteConfigPath: __dirname, siteConfig: { presets: [ - path.join(__dirname, '__fixtures__/preset-plugins.js'), - path.join(__dirname, '__fixtures__/preset-themes.js'), + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), + path.join(__dirname, '__fixtures__/presets/preset-themes.js'), ], }, } as LoadContext; @@ -56,7 +58,9 @@ describe('loadPresets', () => { const context = { siteConfigPath: __dirname, siteConfig: { - presets: [[path.join(__dirname, '__fixtures__/preset-plugins.js')]], + presets: [ + [path.join(__dirname, '__fixtures__/presets/preset-plugins.js')], + ], }, } as Partial; const presets = await loadPresets(context); @@ -69,7 +73,7 @@ describe('loadPresets', () => { siteConfig: { presets: [ [ - path.join(__dirname, '__fixtures__/preset-plugins.js'), + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), {docs: {path: '../'}}, ], ], @@ -85,11 +89,11 @@ describe('loadPresets', () => { siteConfig: { presets: [ [ - path.join(__dirname, '__fixtures__/preset-plugins.js'), + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), {docs: {path: '../'}}, ], [ - path.join(__dirname, '__fixtures__/preset-themes.js'), + path.join(__dirname, '__fixtures__/presets/preset-themes.js'), {algolia: {trackingID: 'foo'}}, ], ], @@ -105,10 +109,10 @@ describe('loadPresets', () => { siteConfig: { presets: [ [ - path.join(__dirname, '__fixtures__/preset-plugins.js'), + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), {docs: {path: '../'}}, ], - path.join(__dirname, '__fixtures__/preset-themes.js'), + path.join(__dirname, '__fixtures__/presets/preset-themes.js'), ], }, } as LoadContext; @@ -122,11 +126,11 @@ describe('loadPresets', () => { siteConfig: { presets: [ [ - path.join(__dirname, '__fixtures__/preset-plugins.js'), + path.join(__dirname, '__fixtures__/presets/preset-plugins.js'), {docs: {path: '../'}}, ], - path.join(__dirname, '__fixtures__/preset-themes.js'), - path.join(__dirname, '__fixtures__/preset-mixed.js'), + path.join(__dirname, '__fixtures__/presets/preset-themes.js'), + path.join(__dirname, '__fixtures__/presets/preset-mixed.js'), ], }, } as LoadContext; diff --git a/packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts b/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts similarity index 71% rename from packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts rename to packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts index de21791fc8..3d0e6a5a8e 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import applyRouteTrailingSlash from '../applyRouteTrailingSlash'; +import {applyRouteTrailingSlash, sortConfig} from '../routeConfig'; import type {RouteConfig} from '@docusaurus/types'; import type {ApplyTrailingSlashParams} from '@docusaurus/utils-common'; @@ -163,3 +163,89 @@ describe('applyRouteTrailingSlash', () => { ).toEqual(route('/abc/?search#anchor', ['/abc/1?search', '/abc/2#anchor'])); }); }); + +describe('sortConfig', () => { + it('sorts route config correctly', () => { + const routes: RouteConfig[] = [ + { + path: '/', + component: '', + routes: [ + {path: '/someDoc', component: ''}, + {path: '/someOtherDoc', component: ''}, + ], + }, + { + path: '/', + component: '', + }, + { + path: '/', + component: '', + routes: [{path: '/subroute', component: ''}], + }, + { + path: '/docs', + component: '', + routes: [ + {path: '/docs/someDoc', component: ''}, + {path: '/docs/someOtherDoc', component: ''}, + ], + }, + { + path: '/community', + component: '', + }, + { + path: '/some-page', + component: '', + }, + ]; + + sortConfig(routes); + + expect(routes).toMatchSnapshot(); + }); + + it('sorts route config given a baseURL', () => { + const baseURL = '/latest/'; + const routes: RouteConfig[] = [ + { + path: baseURL, + component: '', + routes: [ + {path: `${baseURL}someDoc`, component: ''}, + {path: `${baseURL}someOtherDoc`, component: ''}, + ], + }, + { + path: `${baseURL}example`, + component: '', + }, + { + path: `${baseURL}docs`, + component: '', + routes: [ + {path: `${baseURL}docs/someDoc`, component: ''}, + {path: `${baseURL}docs/someOtherDoc`, component: ''}, + ], + }, + { + path: `${baseURL}community`, + component: '', + }, + { + path: `${baseURL}some-page`, + component: '', + }, + { + path: `${baseURL}`, + component: '', + }, + ]; + + sortConfig(routes, baseURL); + + expect(routes).toMatchSnapshot(); + }); +}); diff --git a/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts b/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts deleted file mode 100644 index d81123d03b..0000000000 --- a/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 type {RouteConfig} from '@docusaurus/types'; -import { - applyTrailingSlash, - type ApplyTrailingSlashParams, -} from '@docusaurus/utils-common'; - -export default function applyRouteTrailingSlash( - route: RouteConfig, - params: ApplyTrailingSlashParams, -): RouteConfig { - return { - ...route, - path: applyTrailingSlash(route.path, params), - ...(route.routes && { - routes: route.routes.map((subroute) => - applyRouteTrailingSlash(subroute, params), - ), - }), - }; -} diff --git a/packages/docusaurus/src/server/plugins/configs.ts b/packages/docusaurus/src/server/plugins/configs.ts new file mode 100644 index 0000000000..94ca87424d --- /dev/null +++ b/packages/docusaurus/src/server/plugins/configs.ts @@ -0,0 +1,55 @@ +/** + * 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 {createRequire} from 'module'; +import {loadPresets} from './presets'; +import {resolveModuleName} from '../moduleShorthand'; +import type {LoadContext, PluginConfig} from '@docusaurus/types'; + +export async function loadPluginConfigs( + context: LoadContext, +): Promise { + const preset = await loadPresets(context); + const {siteConfig, siteConfigPath} = context; + const require = createRequire(siteConfigPath); + function normalizeShorthand( + pluginConfig: PluginConfig, + pluginType: 'plugin' | 'theme', + ): PluginConfig { + if (typeof pluginConfig === 'string') { + return resolveModuleName(pluginConfig, require, pluginType); + } else if ( + Array.isArray(pluginConfig) && + typeof pluginConfig[0] === 'string' + ) { + return [ + resolveModuleName(pluginConfig[0], require, pluginType), + pluginConfig[1] ?? {}, + ]; + } + return pluginConfig; + } + preset.plugins = preset.plugins.map((plugin) => + normalizeShorthand(plugin, 'plugin'), + ); + preset.themes = preset.themes.map((theme) => + normalizeShorthand(theme, 'theme'), + ); + const standalonePlugins = siteConfig.plugins.map((plugin) => + normalizeShorthand(plugin, 'plugin'), + ); + const standaloneThemes = siteConfig.themes.map((theme) => + normalizeShorthand(theme, 'theme'), + ); + return [ + ...preset.plugins, + ...preset.themes, + // Site config should be the highest priority. + ...standalonePlugins, + ...standaloneThemes, + ]; +} diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 169bd81115..08002e8db7 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -10,7 +10,6 @@ import fs from 'fs-extra'; import path from 'path'; import type { LoadContext, - PluginConfig, PluginContentLoadedActions, RouteConfig, AllContent, @@ -21,69 +20,26 @@ import type { InitializedPlugin, PluginRouteContext, } from '@docusaurus/types'; -import initPlugins from './init'; +import {initPlugins} from './init'; +import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic'; import logger from '@docusaurus/logger'; import _ from 'lodash'; import {localizePluginTranslationFile} from '../translations/translations'; -import applyRouteTrailingSlash from './applyRouteTrailingSlash'; +import {applyRouteTrailingSlash, sortConfig} from './routeConfig'; -export function sortConfig( - routeConfigs: RouteConfig[], - baseUrl: string = '/', -): void { - // Sort the route config. This ensures that route with nested - // routes is always placed last. - routeConfigs.sort((a, b) => { - // Root route should get placed last. - if (a.path === baseUrl && b.path !== baseUrl) { - return 1; - } - if (a.path !== baseUrl && b.path === baseUrl) { - return -1; - } - - if (a.routes && !b.routes) { - return 1; - } - if (!a.routes && b.routes) { - return -1; - } - // Higher priority get placed first. - if (a.priority || b.priority) { - const priorityA = a.priority || 0; - const priorityB = b.priority || 0; - const score = priorityB - priorityA; - - if (score !== 0) { - return score; - } - } - - return a.path.localeCompare(b.path); - }); - - routeConfigs.forEach((routeConfig) => { - routeConfig.routes?.sort((a, b) => a.path.localeCompare(b.path)); - }); -} - -export async function loadPlugins({ - pluginConfigs, - context, -}: { - pluginConfigs: PluginConfig[]; - context: LoadContext; -}): Promise<{ +export async function loadPlugins(context: LoadContext): Promise<{ plugins: LoadedPlugin[]; pluginsRouteConfigs: RouteConfig[]; globalData: GlobalData; themeConfigTranslated: ThemeConfig; }> { // 1. Plugin Lifecycle - Initialization/Constructor. - const plugins: InitializedPlugin[] = await initPlugins({ - pluginConfigs, - context, - }); + const plugins: InitializedPlugin[] = await initPlugins(context); + + plugins.push( + createBootstrapPlugin(context), + createMDXFallbackPlugin(context), + ); // 2. Plugin Lifecycle - loadContent. // Currently plugins run lifecycle methods in parallel and are not diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 410eb96f7c..326d041e8d 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -18,12 +18,13 @@ import type { InitializedPlugin, } from '@docusaurus/types'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; -import {getPluginVersion} from '../versions'; +import {getPluginVersion} from '../siteMetadata'; import {ensureUniquePluginInstanceIds} from './pluginIds'; import { normalizePluginOptions, normalizeThemeConfig, } from '@docusaurus/utils-validation'; +import {loadPluginConfigs} from './configs'; export type NormalizedPluginConfig = { plugin: PluginModule; @@ -134,16 +135,13 @@ function getThemeValidationFunction( return normalizedPluginConfig.plugin.validateThemeConfig; } -export default async function initPlugins({ - pluginConfigs, - context, -}: { - pluginConfigs: PluginConfig[]; - context: LoadContext; -}): Promise { +export async function initPlugins( + context: LoadContext, +): Promise { // We need to resolve plugins from the perspective of the siteDir, since the // siteDir's package.json declares the dependency on these plugins. const pluginRequire = createRequire(context.siteConfigPath); + const pluginConfigs = await loadPluginConfigs(context); const pluginConfigsNormalized = await normalizePluginConfigs( pluginConfigs, context.siteConfigPath, diff --git a/packages/docusaurus/src/server/presets/index.ts b/packages/docusaurus/src/server/plugins/presets.ts similarity index 95% rename from packages/docusaurus/src/server/presets/index.ts rename to packages/docusaurus/src/server/plugins/presets.ts index e32290f510..d920ea2e59 100644 --- a/packages/docusaurus/src/server/presets/index.ts +++ b/packages/docusaurus/src/server/plugins/presets.ts @@ -14,7 +14,7 @@ import type { } from '@docusaurus/types'; import {resolveModuleName} from '../moduleShorthand'; -export default async function loadPresets(context: LoadContext): Promise<{ +export async function loadPresets(context: LoadContext): Promise<{ plugins: PluginConfig[]; themes: PluginConfig[]; }> { diff --git a/packages/docusaurus/src/server/plugins/routeConfig.ts b/packages/docusaurus/src/server/plugins/routeConfig.ts new file mode 100644 index 0000000000..63fad7cd52 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/routeConfig.ts @@ -0,0 +1,67 @@ +/** + * 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 type {RouteConfig} from '@docusaurus/types'; +import { + applyTrailingSlash, + type ApplyTrailingSlashParams, +} from '@docusaurus/utils-common'; + +export function applyRouteTrailingSlash( + route: RouteConfig, + params: ApplyTrailingSlashParams, +): RouteConfig { + return { + ...route, + path: applyTrailingSlash(route.path, params), + ...(route.routes && { + routes: route.routes.map((subroute) => + applyRouteTrailingSlash(subroute, params), + ), + }), + }; +} + +export function sortConfig( + routeConfigs: RouteConfig[], + baseUrl: string = '/', +): void { + // Sort the route config. This ensures that route with nested + // routes is always placed last. + routeConfigs.sort((a, b) => { + // Root route should get placed last. + if (a.path === baseUrl && b.path !== baseUrl) { + return 1; + } + if (a.path !== baseUrl && b.path === baseUrl) { + return -1; + } + + if (a.routes && !b.routes) { + return 1; + } + if (!a.routes && b.routes) { + return -1; + } + // Higher priority get placed first. + if (a.priority || b.priority) { + const priorityA = a.priority || 0; + const priorityB = b.priority || 0; + const score = priorityB - priorityA; + + if (score !== 0) { + return score; + } + } + + return a.path.localeCompare(b.path); + }); + + routeConfigs.forEach((routeConfig) => { + routeConfig.routes?.sort((a, b) => a.path.localeCompare(b.path)); + }); +} diff --git a/packages/docusaurus/src/server/plugins/synthetic.ts b/packages/docusaurus/src/server/plugins/synthetic.ts new file mode 100644 index 0000000000..f44fad1eb3 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/synthetic.ts @@ -0,0 +1,129 @@ +/** + * 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 admonitions from 'remark-admonitions'; +import type {RuleSetRule} from 'webpack'; +import type {HtmlTagObject, LoadedPlugin, LoadContext} from '@docusaurus/types'; + +/** + * Make a synthetic plugin to: + * - Inject site client modules + * - Inject scripts/stylesheets + */ +export function createBootstrapPlugin({ + siteDir, + siteConfig, +}: LoadContext): LoadedPlugin { + const { + stylesheets, + scripts, + clientModules: siteConfigClientModules, + } = siteConfig; + return { + name: 'docusaurus-bootstrap-plugin', + content: null, + options: { + id: 'default', + }, + version: {type: 'synthetic'}, + path: siteDir, + getClientModules() { + return siteConfigClientModules; + }, + injectHtmlTags: () => { + const stylesheetsTags = stylesheets.map((source) => + typeof source === 'string' + ? `` + : ({ + tagName: 'link', + attributes: { + rel: 'stylesheet', + ...source, + }, + } as HtmlTagObject), + ); + const scriptsTags = scripts.map((source) => + typeof source === 'string' + ? `` + : ({ + tagName: 'script', + attributes: { + ...source, + }, + } as HtmlTagObject), + ); + return { + headTags: [...stylesheetsTags, ...scriptsTags], + }; + }, + }; +} + +/** + * Configure Webpack fallback mdx loader for md/mdx files out of content-plugin + * folders. Adds a "fallback" mdx loader for mdx files that are not processed by + * content plugins. This allows to do things such as importing repo/README.md as + * a partial from another doc. Not ideal solution, but good enough for now + */ +export function createMDXFallbackPlugin({ + siteDir, + siteConfig, +}: LoadContext): LoadedPlugin { + return { + name: 'docusaurus-mdx-fallback-plugin', + content: null, + options: { + id: 'default', + }, + version: {type: 'synthetic'}, + // Synthetic, the path doesn't matter much + path: '.', + configureWebpack(config, isServer, {getJSLoader}) { + // We need the mdx fallback loader to exclude files that were already + // processed by content plugins mdx loaders. This works, but a bit + // hacky... Not sure there's a way to handle that differently in webpack + function getMDXFallbackExcludedPaths(): string[] { + const rules: RuleSetRule[] = config?.module?.rules as RuleSetRule[]; + return rules.flatMap((rule) => { + const isMDXRule = + rule.test instanceof RegExp && rule.test.test('x.mdx'); + return isMDXRule ? (rule.include as string[]) : []; + }); + } + const mdxLoaderOptions = { + staticDirs: siteConfig.staticDirectories.map((dir) => + path.resolve(siteDir, dir), + ), + siteDir, + // External MDX files are always meant to be imported as partials + isMDXPartial: () => true, + // External MDX files might have front matter, just disable the warning + isMDXPartialFrontMatterWarningDisabled: true, + remarkPlugins: [admonitions], + }; + + return { + module: { + rules: [ + { + test: /\.mdx?$/i, + exclude: getMDXFallbackExcludedPaths(), + use: [ + getJSLoader({isServer}), + { + loader: require.resolve('@docusaurus/mdx-loader'), + options: mdxLoaderOptions, + }, + ], + }, + ], + }, + }; + }, + }; +} diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index b6221ec4b2..3bf9f8b37a 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -119,7 +119,7 @@ function getModulePath(target: Module): string { return `${target.path}${queryStr}`; } -export default async function loadRoutes( +export async function loadRoutes( pluginsRouteConfigs: RouteConfig[], baseUrl: string, ): Promise<{ diff --git a/packages/docusaurus/src/server/versions/index.ts b/packages/docusaurus/src/server/siteMetadata.ts similarity index 50% rename from packages/docusaurus/src/server/versions/index.ts rename to packages/docusaurus/src/server/siteMetadata.ts index 54a3777fd1..996244a941 100644 --- a/packages/docusaurus/src/server/versions/index.ts +++ b/packages/docusaurus/src/server/siteMetadata.ts @@ -5,11 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import type {PluginVersionInformation} from '@docusaurus/types'; +import type { + LoadedPlugin, + PluginVersionInformation, + DocusaurusSiteMetadata, +} from '@docusaurus/types'; import fs from 'fs-extra'; import path from 'path'; +import logger from '@docusaurus/logger'; -export async function getPackageJsonVersion( +async function getPackageJsonVersion( packageJsonPath: string, ): Promise { if (await fs.pathExists(packageJsonPath)) { @@ -59,3 +64,52 @@ export async function getPluginVersion( // package.json (e.g. inline plugin), we can only classify it as local. return {type: 'local'}; } + +/** + * We want all `@docusaurus/*` packages to have the exact same version! + * @see https://github.com/facebook/docusaurus/issues/3371 + * @see https://github.com/facebook/docusaurus/pull/3386 + */ +function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) { + const {docusaurusVersion} = siteMetadata; + Object.entries(siteMetadata.pluginVersions).forEach( + ([plugin, versionInfo]) => { + if ( + versionInfo.type === 'package' && + versionInfo.name?.startsWith('@docusaurus/') && + versionInfo.version && + versionInfo.version !== docusaurusVersion + ) { + // should we throw instead? + // It still could work with different versions + logger.error`Invalid name=${plugin} version number=${versionInfo.version}. +All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${docusaurusVersion}). +Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`; + } + }, + ); +} + +export async function loadSiteMetadata({ + plugins, + siteDir, +}: { + plugins: LoadedPlugin[]; + siteDir: string; +}): Promise { + const siteMetadata: DocusaurusSiteMetadata = { + docusaurusVersion: (await getPackageJsonVersion( + path.join(__dirname, '../../package.json'), + ))!, + siteVersion: await getPackageJsonVersion( + path.join(siteDir, 'package.json'), + ), + pluginVersions: Object.fromEntries( + plugins + .filter(({version: {type}}) => type !== 'synthetic') + .map(({name, version}) => [name, version]), + ), + }; + checkDocusaurusPackagesVersion(siteMetadata); + return siteMetadata; +} diff --git a/packages/docusaurus/src/server/themes/__tests__/alias.test.ts b/packages/docusaurus/src/server/themes/__tests__/alias.test.ts index b0a22e9c15..72ed394515 100644 --- a/packages/docusaurus/src/server/themes/__tests__/alias.test.ts +++ b/packages/docusaurus/src/server/themes/__tests__/alias.test.ts @@ -7,7 +7,7 @@ import path from 'path'; import fs from 'fs-extra'; -import themeAlias, {sortAliases} from '../alias'; +import {themeAlias, sortAliases} from '../alias'; describe('sortAliases', () => { // https://github.com/facebook/docusaurus/issues/6878 diff --git a/packages/docusaurus/src/server/themes/alias.ts b/packages/docusaurus/src/server/themes/alias.ts index 0a63d80a83..c5ee69aa05 100644 --- a/packages/docusaurus/src/server/themes/alias.ts +++ b/packages/docusaurus/src/server/themes/alias.ts @@ -26,7 +26,7 @@ export function sortAliases(aliases: ThemeAliases): ThemeAliases { return Object.fromEntries(entries); } -export default async function themeAlias( +export async function themeAlias( themePath: string, addOriginalAlias: boolean, ): Promise { diff --git a/packages/docusaurus/src/server/themes/index.ts b/packages/docusaurus/src/server/themes/index.ts index 071acd007a..dbb8ca34e1 100644 --- a/packages/docusaurus/src/server/themes/index.ts +++ b/packages/docusaurus/src/server/themes/index.ts @@ -5,10 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types'; import path from 'path'; import {THEME_PATH} from '@docusaurus/utils'; -import themeAlias, {sortAliases} from './alias'; +import {themeAlias, sortAliases} from './alias'; +import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types'; const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback'); diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index ba749f0aef..a989378a0c 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -8,12 +8,6 @@ import path from 'path'; import fs from 'fs-extra'; import _ from 'lodash'; -import type { - TranslationFileContent, - TranslationFile, - TranslationMessage, - InitializedPlugin, -} from '@docusaurus/types'; import { getPluginI18nPath, toMessageRelativeFilePath, @@ -22,6 +16,12 @@ import { } from '@docusaurus/utils'; import {Joi} from '@docusaurus/utils-validation'; import logger from '@docusaurus/logger'; +import type { + TranslationFileContent, + TranslationFile, + TranslationMessage, + InitializedPlugin, +} from '@docusaurus/types'; export type WriteTranslationsOptions = { override?: boolean;