mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
refactor(core): reorganize functions (#7037)
This commit is contained in:
parent
c81d21a641
commit
85a79fd9b9
64 changed files with 1207 additions and 1304 deletions
|
@ -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';
|
||||
|
|
|
@ -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/');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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, '/']);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export {
|
|||
mergeTranslations,
|
||||
updateTranslationFileMessages,
|
||||
getPluginI18nPath,
|
||||
localizePath,
|
||||
} from './i18nUtils';
|
||||
export {
|
||||
removeSuffix,
|
||||
|
|
|
@ -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<BuildCLIOptions> = {},
|
||||
// When running build, we force terminate the process to prevent async
|
||||
|
|
|
@ -26,7 +26,7 @@ async function removePath(entry: {path: string; description: string}) {
|
|||
}
|
||||
}
|
||||
|
||||
export default async function clear(siteDir: string): Promise<unknown> {
|
||||
export async function clear(siteDir: string): Promise<unknown> {
|
||||
const generatedFolder = {
|
||||
path: path.join(siteDir, GENERATED_FILES_DIR_NAME),
|
||||
description: 'generated folder',
|
||||
|
|
|
@ -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<BuildCLIOptions> = {},
|
||||
): Promise<void> {
|
||||
|
|
|
@ -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<void> {
|
||||
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) => {
|
||||
|
|
|
@ -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<void> {
|
||||
|
|
|
@ -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<StartCLIOptions>,
|
||||
): Promise<void> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<SwizzleContext> {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<string[]> {
|
||||
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,
|
||||
|
|
|
@ -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<void> {
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
107
packages/docusaurus/src/server/__tests__/clientModules.test.ts
Normal file
107
packages/docusaurus/src/server/__tests__/clientModules.test.ts
Normal file
|
@ -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(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('loads multiple non-empty plugins', () => {
|
||||
const clientModules = loadClientModules([pluginFooBar, pluginHelloWorld]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/bar",
|
||||
"/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/world",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('loads multiple non-empty plugins in different order', () => {
|
||||
const clientModules = loadClientModules([pluginHelloWorld, pluginFooBar]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('loads both empty and non-empty plugins', () => {
|
||||
const clientModules = loadClientModules([
|
||||
pluginHelloWorld,
|
||||
pluginEmpty,
|
||||
pluginFooBar,
|
||||
]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
|
||||
"<PROJECT_ROOT>/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",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import loadConfig from '../config';
|
||||
import {loadConfig} from '../config';
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('website with valid siteConfig', async () => {
|
||||
|
|
224
packages/docusaurus/src/server/__tests__/htmlTags.test.ts
Normal file
224
packages/docusaurus/src/server/__tests__/htmlTags.test.ts
Normal file
|
@ -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',
|
||||
},
|
||||
'<script>window.alert(1);</script>',
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"Docusaurus\\">
|
||||
<script type=\\"text/javascript\\" src=\\"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js\\" async data-options=\\"{"prop":true}\\"></script>",
|
||||
"postBodyTags": "",
|
||||
"preBodyTags": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('only injects preBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPreBodyTags]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
"postBodyTags": "",
|
||||
"preBodyTags": "<script type=\\"text/javascript\\">window.foo = null;</script>",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('only injects postBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPostBodyTags]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
"postBodyTags": "<div>Test content</div>
|
||||
<script>window.alert(1);</script>",
|
||||
"preBodyTags": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows multiple plugins that inject different part of html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
pluginHeadTags,
|
||||
pluginPostBodyTags,
|
||||
pluginPreBodyTags,
|
||||
]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"Docusaurus\\">
|
||||
<script type=\\"text/javascript\\" src=\\"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js\\" async data-options=\\"{"prop":true}\\"></script>",
|
||||
"postBodyTags": "<div>Test content</div>
|
||||
<script>window.alert(1);</script>",
|
||||
"preBodyTags": "<script type=\\"text/javascript\\">window.foo = null;</script>",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows multiple plugins that might/might not inject html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
pluginEmpty,
|
||||
pluginHeadTags,
|
||||
pluginPostBodyTags,
|
||||
pluginMaybeInjectHeadTags,
|
||||
]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"Docusaurus\\">
|
||||
<script type=\\"text/javascript\\" src=\\"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js\\" async data-options=\\"{"prop":true}\\"></script>",
|
||||
"postBodyTags": "<div>Test content</div>
|
||||
<script>window.alert(1);</script>",
|
||||
"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."`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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/');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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'];
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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'];
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('multiple non-empty', () => {
|
||||
const clientModules = loadClientModules([
|
||||
pluginFooBar(),
|
||||
pluginHelloWorld(),
|
||||
]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('multiple non-empty different order', () => {
|
||||
const clientModules = loadClientModules([
|
||||
pluginHelloWorld(),
|
||||
pluginFooBar(),
|
||||
]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('empty and non-empty', () => {
|
||||
const clientModules = loadClientModules([
|
||||
pluginHelloWorld(),
|
||||
pluginEmpty(),
|
||||
pluginFooBar(),
|
||||
]);
|
||||
expect(clientModules).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo",
|
||||
"<PROJECT_ROOT>/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(`
|
||||
[
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/hello",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/world",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/foo",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/server/client-modules/__tests__/__fixtures__/bar",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -8,9 +8,7 @@
|
|||
import path from 'path';
|
||||
import type {LoadedPlugin} from '@docusaurus/types';
|
||||
|
||||
export default function loadClientModules(
|
||||
plugins: LoadedPlugin<unknown>[],
|
||||
): string[] {
|
||||
export function loadClientModules(plugins: LoadedPlugin<unknown>[]): string[] {
|
||||
return plugins.flatMap(
|
||||
(plugin) =>
|
||||
plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ??
|
|
@ -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<DocusaurusConfig> {
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
`<meta name="generator" content="docusaurus">`,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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;',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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(
|
||||
`"<script type=\\"text/javascript\\" src=\\"https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js\\" async data-options=\\"{"prop":true}\\"></script>"`,
|
||||
);
|
||||
|
||||
expect(
|
||||
htmlTagObjectToString({
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'preconnect',
|
||||
href: 'www.google-analytics.com',
|
||||
},
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">"`,
|
||||
);
|
||||
|
||||
expect(
|
||||
htmlTagObjectToString({
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: 'background-color:lightblue',
|
||||
},
|
||||
innerHTML: 'Lightblue color here',
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div style=\\"background-color:lightblue\\">Lightblue color here</div>"`,
|
||||
);
|
||||
|
||||
expect(
|
||||
htmlTagObjectToString({
|
||||
tagName: 'div',
|
||||
innerHTML: 'Test',
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"<div>Test</div>"`);
|
||||
});
|
||||
|
||||
it('valid html void tag', () => {
|
||||
expect(
|
||||
htmlTagObjectToString({
|
||||
tagName: 'meta',
|
||||
attributes: {
|
||||
name: 'generator',
|
||||
content: 'Docusaurus',
|
||||
},
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<meta name=\\"generator\\" content=\\"Docusaurus\\">"`,
|
||||
);
|
||||
|
||||
expect(
|
||||
htmlTagObjectToString({
|
||||
tagName: 'img',
|
||||
attributes: {
|
||||
src: '/img/docusaurus.png',
|
||||
alt: 'Docusaurus logo',
|
||||
height: '42',
|
||||
width: '42',
|
||||
},
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<img src=\\"/img/docusaurus.png\\" alt=\\"Docusaurus logo\\" height=\\"42\\" width=\\"42\\">"`,
|
||||
);
|
||||
});
|
||||
|
||||
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."`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"docusaurus\\">",
|
||||
"postBodyTags": "",
|
||||
"preBodyTags": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('only inject preBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPreBodyTags()]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
"postBodyTags": "",
|
||||
"preBodyTags": "<script type=\\"text/javascript\\">window.foo = null;</script>",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('only inject postBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPostBodyTags()]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
"postBodyTags": "<div>Test content</div>",
|
||||
"preBodyTags": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('multiple plugins that inject different part of html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
pluginHeadTags(),
|
||||
pluginPostBodyTags(),
|
||||
pluginPreBodyTags(),
|
||||
]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"docusaurus\\">",
|
||||
"postBodyTags": "<div>Test content</div>",
|
||||
"preBodyTags": "<script type=\\"text/javascript\\">window.foo = null;</script>",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('multiple plugins that might/might not inject html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
pluginEmpty(),
|
||||
pluginHeadTags(),
|
||||
pluginPostBodyTags(),
|
||||
]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "<link rel=\\"preconnect\\" href=\\"www.google-analytics.com\\">
|
||||
<meta name=\\"generator\\" content=\\"docusaurus\\">",
|
||||
"postBodyTags": "<div>Test content</div>",
|
||||
"preBodyTags": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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 ? '' : `</${tagDefinition.tagName}>`}`;
|
||||
}
|
|
@ -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(),
|
||||
};
|
||||
}
|
81
packages/docusaurus/src/server/htmlTags.ts
Normal file
81
packages/docusaurus/src/server/htmlTags.ts
Normal file
|
@ -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 ? '' : `</${tag.tagName}>`;
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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, '/']);
|
||||
}
|
||||
|
|
|
@ -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<PluginConfig[]> {
|
||||
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'
|
||||
? `<link rel="stylesheet" href="${source}">`
|
||||
: ({
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'stylesheet',
|
||||
...source,
|
||||
},
|
||||
} as HtmlTagObject),
|
||||
);
|
||||
const scriptsTags = scripts.map((source) =>
|
||||
typeof source === 'string'
|
||||
? `<script src="${source}"></script>`
|
||||
: ({
|
||||
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?`;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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": "<PROJECT_ROOT>/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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -6,15 +6,23 @@
|
|||
*/
|
||||
|
||||
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: [
|
||||
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',
|
||||
|
@ -26,6 +34,8 @@ describe('loadPlugins', () => {
|
|||
actions.setGlobalData({content, prop: this.prop});
|
||||
},
|
||||
}),
|
||||
],
|
||||
themes: [
|
||||
() => ({
|
||||
name: 'test2',
|
||||
configureWebpack() {
|
||||
|
@ -33,107 +43,9 @@ describe('loadPlugins', () => {
|
|||
},
|
||||
}),
|
||||
],
|
||||
|
||||
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'),
|
||||
},
|
||||
}),
|
||||
).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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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<LoadContext>;
|
||||
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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
55
packages/docusaurus/src/server/plugins/configs.ts
Normal file
55
packages/docusaurus/src/server/plugins/configs.ts
Normal file
|
@ -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<PluginConfig[]> {
|
||||
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,
|
||||
];
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<InitializedPlugin[]> {
|
||||
export async function initPlugins(
|
||||
context: LoadContext,
|
||||
): Promise<InitializedPlugin[]> {
|
||||
// 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,
|
||||
|
|
|
@ -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[];
|
||||
}> {
|
67
packages/docusaurus/src/server/plugins/routeConfig.ts
Normal file
67
packages/docusaurus/src/server/plugins/routeConfig.ts
Normal file
|
@ -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));
|
||||
});
|
||||
}
|
129
packages/docusaurus/src/server/plugins/synthetic.ts
Normal file
129
packages/docusaurus/src/server/plugins/synthetic.ts
Normal file
|
@ -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'
|
||||
? `<link rel="stylesheet" href="${source}">`
|
||||
: ({
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'stylesheet',
|
||||
...source,
|
||||
},
|
||||
} as HtmlTagObject),
|
||||
);
|
||||
const scriptsTags = scripts.map((source) =>
|
||||
typeof source === 'string'
|
||||
? `<script src="${source}"></script>`
|
||||
: ({
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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<{
|
||||
|
|
|
@ -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<string | undefined> {
|
||||
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<DocusaurusSiteMetadata> {
|
||||
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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<ThemeAliases> {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue