refactor(core): reorganize functions (#7037)

This commit is contained in:
Joshua Chen 2022-03-28 17:12:36 +08:00 committed by GitHub
parent c81d21a641
commit 85a79fd9b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 1207 additions and 1304 deletions

View file

@ -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';

View file

@ -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/');
});
});

View file

@ -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, '/']);
}

View file

@ -32,6 +32,7 @@ export {
mergeTranslations,
updateTranslationFileMessages,
getPluginI18nPath,
localizePath,
} from './i18nUtils';
export {
removeSuffix,

View file

@ -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

View file

@ -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',

View file

@ -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> {

View file

@ -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) => {

View file

@ -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> {

View file

@ -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> {

View file

@ -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';

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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';

View 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",
]
`);
});
});

View file

@ -6,7 +6,7 @@
*/
import path from 'path';
import loadConfig from '../config';
import {loadConfig} from '../config';
describe('loadConfig', () => {
it('website with valid siteConfig', async () => {

View 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=\\"{&quot;prop&quot;: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=\\"{&quot;prop&quot;: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=\\"{&quot;prop&quot;: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."`,
);
});
});

View file

@ -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/');
});
});

View file

@ -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', () => {

View file

@ -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'});
});
});

View file

@ -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,
};
};

View file

@ -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'];
},
};
};

View file

@ -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'];
},
};
};

View file

@ -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",
]
`);
});
});

View file

@ -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)) ??

View file

@ -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))) {

View file

@ -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',
};
};

View file

@ -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">`,
],
};
},
};
};

View file

@ -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',
},
],
};
},
};
};

View file

@ -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;',
},
};
},
};
};

View file

@ -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=\\"{&quot;prop&quot;: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."`,
);
});
});

View file

@ -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": "",
}
`);
});
});

View file

@ -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}>`}`;
}

View file

@ -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(),
};
}

View 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(),
),
),
);
}

View file

@ -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, '/']);
}

View file

@ -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?`;
}
},
);
}

View 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",
},
],
},
]
`;

View file

@ -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",
},
],
},
]
`;

View file

@ -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();
});
});

View file

@ -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};
}

View file

@ -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;

View file

@ -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();
});
});

View file

@ -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),
),
}),
};
}

View 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,
];
}

View file

@ -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

View file

@ -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,

View file

@ -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[];
}> {

View 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));
});
}

View 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,
},
],
},
],
},
};
},
};
}

View file

@ -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<{

View file

@ -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;
}

View file

@ -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

View file

@ -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> {

View file

@ -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');

View file

@ -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;