mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 10:48:05 +02:00
refactor(core): reorganize files (#7042)
* refactor(core): reorganize files * fix types
This commit is contained in:
parent
85a79fd9b9
commit
5fb09a2946
61 changed files with 1089 additions and 1028 deletions
|
@ -20,9 +20,9 @@ declare module '@generated/docusaurus.config' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@generated/site-metadata' {
|
declare module '@generated/site-metadata' {
|
||||||
import type {DocusaurusSiteMetadata} from '@docusaurus/types';
|
import type {SiteMetadata} from '@docusaurus/types';
|
||||||
|
|
||||||
const siteMetadata: DocusaurusSiteMetadata;
|
const siteMetadata: SiteMetadata;
|
||||||
export = siteMetadata;
|
export = siteMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {BlogContent, BlogPaginated} from './types';
|
import type {BlogContent, BlogPaginated} from './types';
|
||||||
import type {TranslationFileContent, TranslationFiles} from '@docusaurus/types';
|
import type {TranslationFileContent, TranslationFile} from '@docusaurus/types';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
|
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
function translateListPage(
|
function translateListPage(
|
||||||
|
@ -27,7 +27,7 @@ function translateListPage(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTranslationFiles(options: PluginOptions): TranslationFiles {
|
export function getTranslationFiles(options: PluginOptions): TranslationFile[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: 'options',
|
path: 'options',
|
||||||
|
@ -51,7 +51,7 @@ export function getTranslationFiles(options: PluginOptions): TranslationFiles {
|
||||||
|
|
||||||
export function translateContent(
|
export function translateContent(
|
||||||
content: BlogContent,
|
content: BlogContent,
|
||||||
translationFiles: TranslationFiles,
|
translationFiles: TranslationFile[],
|
||||||
): BlogContent {
|
): BlogContent {
|
||||||
const {content: optionsTranslations} = translationFiles[0]!;
|
const {content: optionsTranslations} = translationFiles[0]!;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -168,7 +168,7 @@ describe('simple site', () => {
|
||||||
loadSiteOptions: {options: Partial<PluginOptions>} = {options: {}},
|
loadSiteOptions: {options: Partial<PluginOptions>} = {options: {}},
|
||||||
) {
|
) {
|
||||||
const siteDir = path.join(fixtureDir, 'simple-site');
|
const siteDir = path.join(fixtureDir, 'simple-site');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const options = {
|
const options = {
|
||||||
id: DEFAULT_PLUGIN_ID,
|
id: DEFAULT_PLUGIN_ID,
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
|
@ -523,7 +523,8 @@ describe('versioned site', () => {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const siteDir = path.join(fixtureDir, 'versioned-site');
|
const siteDir = path.join(fixtureDir, 'versioned-site');
|
||||||
const context = await loadContext(siteDir, {
|
const context = await loadContext({
|
||||||
|
siteDir,
|
||||||
locale: loadSiteOptions.locale,
|
locale: loadSiteOptions.locale,
|
||||||
});
|
});
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
@ -115,7 +115,7 @@ Entries created:
|
||||||
describe('sidebar', () => {
|
describe('sidebar', () => {
|
||||||
it('site with wrong sidebar content', async () => {
|
it('site with wrong sidebar content', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
|
@ -131,7 +131,7 @@ describe('sidebar', () => {
|
||||||
|
|
||||||
it('site with wrong sidebar file path', async () => {
|
it('site with wrong sidebar file path', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
|
@ -155,7 +155,7 @@ describe('sidebar', () => {
|
||||||
|
|
||||||
it('site with undefined sidebar', async () => {
|
it('site with undefined sidebar', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -173,7 +173,7 @@ describe('sidebar', () => {
|
||||||
|
|
||||||
it('site with disabled sidebar', async () => {
|
it('site with disabled sidebar', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -194,7 +194,7 @@ describe('empty/no docs website', () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');
|
||||||
|
|
||||||
it('no files in docs folder', async () => {
|
it('no files in docs folder', async () => {
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
await fs.ensureDir(path.join(siteDir, 'docs'));
|
await fs.ensureDir(path.join(siteDir, 'docs'));
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
|
@ -208,7 +208,7 @@ describe('empty/no docs website', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('docs folder does not exist', async () => {
|
it('docs folder does not exist', async () => {
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
await expect(
|
await expect(
|
||||||
pluginContentDocs(
|
pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
|
@ -228,7 +228,7 @@ describe('empty/no docs website', () => {
|
||||||
describe('simple website', () => {
|
describe('simple website', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
|
@ -341,7 +341,7 @@ describe('simple website', () => {
|
||||||
describe('versioned website', () => {
|
describe('versioned website', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
const routeBasePath = 'docs';
|
const routeBasePath = 'docs';
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
|
@ -470,7 +470,7 @@ describe('versioned website', () => {
|
||||||
describe('versioned website (community)', () => {
|
describe('versioned website (community)', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const sidebarPath = path.join(siteDir, 'community_sidebars.json');
|
const sidebarPath = path.join(siteDir, 'community_sidebars.json');
|
||||||
const routeBasePath = 'community';
|
const routeBasePath = 'community';
|
||||||
const pluginId = 'community';
|
const pluginId = 'community';
|
||||||
|
@ -578,7 +578,7 @@ describe('versioned website (community)', () => {
|
||||||
describe('site with doc label', () => {
|
describe('site with doc label', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
|
@ -620,7 +620,7 @@ describe('site with full autogenerated sidebar', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'site-with-autogenerated-sidebar',
|
'site-with-autogenerated-sidebar',
|
||||||
);
|
);
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -675,7 +675,7 @@ describe('site with partial autogenerated sidebars', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'site-with-autogenerated-sidebar',
|
'site-with-autogenerated-sidebar',
|
||||||
);
|
);
|
||||||
const context = await loadContext(siteDir, {});
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -731,7 +731,7 @@ describe('site with partial autogenerated sidebars 2 (fix #4638)', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'site-with-autogenerated-sidebar',
|
'site-with-autogenerated-sidebar',
|
||||||
);
|
);
|
||||||
const context = await loadContext(siteDir, {});
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -768,7 +768,7 @@ describe('site with custom sidebar items generator', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'site-with-autogenerated-sidebar',
|
'site-with-autogenerated-sidebar',
|
||||||
);
|
);
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentDocs(
|
const plugin = await pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
|
|
@ -22,7 +22,6 @@ import {
|
||||||
import type {
|
import type {
|
||||||
TranslationFileContent,
|
TranslationFileContent,
|
||||||
TranslationFile,
|
TranslationFile,
|
||||||
TranslationFiles,
|
|
||||||
TranslationMessage,
|
TranslationMessage,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import {mergeTranslations} from '@docusaurus/utils';
|
import {mergeTranslations} from '@docusaurus/utils';
|
||||||
|
@ -242,7 +241,7 @@ function translateSidebars(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersionTranslationFiles(version: LoadedVersion): TranslationFiles {
|
function getVersionTranslationFiles(version: LoadedVersion): TranslationFile[] {
|
||||||
const versionTranslations: TranslationFileContent = {
|
const versionTranslations: TranslationFileContent = {
|
||||||
'version.label': {
|
'version.label': {
|
||||||
message: version.label,
|
message: version.label,
|
||||||
|
@ -283,7 +282,7 @@ function translateVersion(
|
||||||
|
|
||||||
function getVersionsTranslationFiles(
|
function getVersionsTranslationFiles(
|
||||||
versions: LoadedVersion[],
|
versions: LoadedVersion[],
|
||||||
): TranslationFiles {
|
): TranslationFile[] {
|
||||||
return versions.flatMap(getVersionTranslationFiles);
|
return versions.flatMap(getVersionTranslationFiles);
|
||||||
}
|
}
|
||||||
function translateVersions(
|
function translateVersions(
|
||||||
|
@ -295,7 +294,7 @@ function translateVersions(
|
||||||
|
|
||||||
export function getLoadedContentTranslationFiles(
|
export function getLoadedContentTranslationFiles(
|
||||||
loadedContent: LoadedContent,
|
loadedContent: LoadedContent,
|
||||||
): TranslationFiles {
|
): TranslationFile[] {
|
||||||
return getVersionsTranslationFiles(loadedContent.loadedVersions);
|
return getVersionsTranslationFiles(loadedContent.loadedVersions);
|
||||||
}
|
}
|
||||||
export function translateLoadedContent(
|
export function translateLoadedContent(
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
describe('docusaurus-plugin-content-pages', () => {
|
describe('docusaurus-plugin-content-pages', () => {
|
||||||
it('loads simple pages', async () => {
|
it('loads simple pages', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentPages(
|
const plugin = await pluginContentPages(
|
||||||
context,
|
context,
|
||||||
validateOptions({
|
validateOptions({
|
||||||
|
@ -32,7 +32,7 @@ describe('docusaurus-plugin-content-pages', () => {
|
||||||
|
|
||||||
it('loads simple pages with french translations', async () => {
|
it('loads simple pages with french translations', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugin = await pluginContentPages(
|
const plugin = await pluginContentPages(
|
||||||
{
|
{
|
||||||
...context,
|
...context,
|
||||||
|
|
206
packages/docusaurus-types/src/index.d.ts
vendored
206
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -10,7 +10,11 @@ import type {CustomizeRuleString} from 'webpack-merge/dist/types';
|
||||||
import type {CommanderStatic} from 'commander';
|
import type {CommanderStatic} from 'commander';
|
||||||
import type {ParsedUrlQueryInput} from 'querystring';
|
import type {ParsedUrlQueryInput} from 'querystring';
|
||||||
import type Joi from 'joi';
|
import type Joi from 'joi';
|
||||||
import type {Overwrite, DeepPartial, DeepRequired} from 'utility-types';
|
import type {
|
||||||
|
Required as RequireKeys,
|
||||||
|
DeepPartial,
|
||||||
|
DeepRequired,
|
||||||
|
} from 'utility-types';
|
||||||
import type {Location} from 'history';
|
import type {Location} from 'history';
|
||||||
import type Loadable from 'react-loadable';
|
import type Loadable from 'react-loadable';
|
||||||
|
|
||||||
|
@ -20,16 +24,23 @@ export type ThemeConfig = {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Docusaurus config, after validation/normalization
|
/**
|
||||||
export interface DocusaurusConfig {
|
* Docusaurus config, after validation/normalization.
|
||||||
|
*/
|
||||||
|
export type DocusaurusConfig = {
|
||||||
|
/**
|
||||||
|
* Always has both leading and trailing slash (`/base/`). May be localized.
|
||||||
|
*/
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
baseUrlIssueBanner: boolean;
|
baseUrlIssueBanner: boolean;
|
||||||
favicon?: string;
|
favicon?: string;
|
||||||
tagline: string;
|
tagline: string;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
// trailingSlash undefined = legacy retrocompatible behavior
|
/**
|
||||||
// /file => /file/index.html
|
* `undefined` = legacy retrocompatible behavior. Usually it means `/file` =>
|
||||||
|
* `/file/index.html`.
|
||||||
|
*/
|
||||||
trailingSlash: boolean | undefined;
|
trailingSlash: boolean | undefined;
|
||||||
i18n: I18nConfig;
|
i18n: I18nConfig;
|
||||||
onBrokenLinks: ReportingSeverity;
|
onBrokenLinks: ReportingSeverity;
|
||||||
|
@ -69,19 +80,16 @@ export interface DocusaurusConfig {
|
||||||
webpack?: {
|
webpack?: {
|
||||||
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule);
|
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule);
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Docusaurus config, as provided by the user (partial/unnormalized)
|
/**
|
||||||
// This type is used to provide type-safety / IDE auto-complete on the config
|
* Docusaurus config, as provided by the user (partial/unnormalized). This type
|
||||||
// file. See https://docusaurus.io/docs/typescript-support
|
* is used to provide type-safety / IDE auto-complete on the config file.
|
||||||
export type Config = Overwrite<
|
* @see https://docusaurus.io/docs/typescript-support
|
||||||
Partial<DocusaurusConfig>,
|
*/
|
||||||
{
|
export type Config = RequireKeys<
|
||||||
title: Required<DocusaurusConfig['title']>;
|
DeepPartial<DocusaurusConfig>,
|
||||||
url: Required<DocusaurusConfig['url']>;
|
'title' | 'url' | 'baseUrl'
|
||||||
baseUrl: Required<DocusaurusConfig['baseUrl']>;
|
|
||||||
i18n?: DeepPartial<DocusaurusConfig['i18n']>;
|
|
||||||
}
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,11 +109,11 @@ export type PluginVersionInformation =
|
||||||
| {readonly type: 'local'}
|
| {readonly type: 'local'}
|
||||||
| {readonly type: 'synthetic'};
|
| {readonly type: 'synthetic'};
|
||||||
|
|
||||||
export interface DocusaurusSiteMetadata {
|
export type SiteMetadata = {
|
||||||
readonly docusaurusVersion: string;
|
readonly docusaurusVersion: string;
|
||||||
readonly siteVersion?: string;
|
readonly siteVersion?: string;
|
||||||
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
|
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Inspired by Chrome JSON, because it's a widely supported i18n format
|
// Inspired by Chrome JSON, because it's a widely supported i18n format
|
||||||
// https://developer.chrome.com/apps/i18n-messages
|
// https://developer.chrome.com/apps/i18n-messages
|
||||||
|
@ -116,7 +124,6 @@ export interface DocusaurusSiteMetadata {
|
||||||
export type TranslationMessage = {message: string; description?: string};
|
export type TranslationMessage = {message: string; description?: string};
|
||||||
export type TranslationFileContent = {[key: string]: TranslationMessage};
|
export type TranslationFileContent = {[key: string]: TranslationMessage};
|
||||||
export type TranslationFile = {path: string; content: TranslationFileContent};
|
export type TranslationFile = {path: string; content: TranslationFileContent};
|
||||||
export type TranslationFiles = TranslationFile[];
|
|
||||||
|
|
||||||
export type I18nLocaleConfig = {
|
export type I18nLocaleConfig = {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -134,9 +141,9 @@ export type I18n = DeepRequired<I18nConfig> & {currentLocale: string};
|
||||||
|
|
||||||
export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
|
export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
|
||||||
|
|
||||||
export interface DocusaurusContext {
|
export type DocusaurusContext = {
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
siteMetadata: DocusaurusSiteMetadata;
|
siteMetadata: SiteMetadata;
|
||||||
globalData: GlobalData;
|
globalData: GlobalData;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
codeTranslations: {[msgId: string]: string};
|
codeTranslations: {[msgId: string]: string};
|
||||||
|
@ -144,12 +151,12 @@ export interface DocusaurusContext {
|
||||||
// Don't put mutable values here, to avoid triggering re-renders
|
// Don't put mutable values here, to avoid triggering re-renders
|
||||||
// We could reconsider that choice if context selectors are implemented
|
// We could reconsider that choice if context selectors are implemented
|
||||||
// isBrowser: boolean; // Not here on purpose!
|
// isBrowser: boolean; // Not here on purpose!
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Preset {
|
export type Preset = {
|
||||||
plugins?: PluginConfig[];
|
plugins?: PluginConfig[];
|
||||||
themes?: PluginConfig[];
|
themes?: PluginConfig[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PresetModule = {
|
export type PresetModule = {
|
||||||
<T>(context: LoadContext, presetOptions: T): Preset;
|
<T>(context: LoadContext, presetOptions: T): Preset;
|
||||||
|
@ -195,38 +202,40 @@ export type BuildCLIOptions = BuildOptions & {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface LoadContext {
|
export type LoadContext = {
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
generatedFilesDir: string;
|
generatedFilesDir: string;
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
siteConfigPath: string;
|
siteConfigPath: string;
|
||||||
outDir: string;
|
outDir: string;
|
||||||
baseUrl: string; // TODO to remove: useless, there's already siteConfig.baseUrl!
|
/**
|
||||||
|
* Duplicated from `siteConfig.baseUrl`, but probably worth keeping. We mutate
|
||||||
|
* `siteConfig` to make `baseUrl` there localized as well, but that's mostly
|
||||||
|
* for client-side. `context.baseUrl` is still more convenient for plugins.
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
ssrTemplate: string;
|
ssrTemplate: string;
|
||||||
codeTranslations: {[msgId: string]: string};
|
codeTranslations: {[msgId: string]: string};
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface InjectedHtmlTags {
|
|
||||||
headTags: string;
|
|
||||||
preBodyTags: string;
|
|
||||||
postBodyTags: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
|
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
|
||||||
|
|
||||||
export interface Props extends LoadContext, InjectedHtmlTags {
|
export type Props = LoadContext & {
|
||||||
readonly siteMetadata: DocusaurusSiteMetadata;
|
readonly headTags: string;
|
||||||
|
readonly preBodyTags: string;
|
||||||
|
readonly postBodyTags: string;
|
||||||
|
readonly siteMetadata: SiteMetadata;
|
||||||
readonly routes: RouteConfig[];
|
readonly routes: RouteConfig[];
|
||||||
readonly routesPaths: string[];
|
readonly routesPaths: string[];
|
||||||
readonly plugins: LoadedPlugin[];
|
readonly plugins: LoadedPlugin[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PluginContentLoadedActions {
|
export type PluginContentLoadedActions = {
|
||||||
addRoute: (config: RouteConfig) => void;
|
addRoute: (config: RouteConfig) => void;
|
||||||
createData: (name: string, data: string) => Promise<string>;
|
createData: (name: string, data: string) => Promise<string>;
|
||||||
setGlobalData: (data: unknown) => void;
|
setGlobalData: (data: unknown) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AllContent = {
|
export type AllContent = {
|
||||||
[pluginName: string]: {
|
[pluginName: string]: {
|
||||||
|
@ -237,7 +246,7 @@ export type AllContent = {
|
||||||
// TODO improve type (not exposed by postcss-loader)
|
// TODO improve type (not exposed by postcss-loader)
|
||||||
export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]};
|
export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]};
|
||||||
|
|
||||||
export interface Plugin<Content = unknown> {
|
export type Plugin<Content = unknown> = {
|
||||||
name: string;
|
name: string;
|
||||||
loadContent?: () => Promise<Content>;
|
loadContent?: () => Promise<Content>;
|
||||||
contentLoaded?: (args: {
|
contentLoaded?: (args: {
|
||||||
|
@ -273,19 +282,56 @@ export interface Plugin<Content = unknown> {
|
||||||
// TODO before/afterDevServer implementation
|
// TODO before/afterDevServer implementation
|
||||||
|
|
||||||
// translations
|
// translations
|
||||||
getTranslationFiles?: (args: {content: Content}) => Promise<TranslationFiles>;
|
getTranslationFiles?: (args: {
|
||||||
|
content: Content;
|
||||||
|
}) => Promise<TranslationFile[]>;
|
||||||
getDefaultCodeTranslationMessages?: () => Promise<{[id: string]: string}>;
|
getDefaultCodeTranslationMessages?: () => Promise<{[id: string]: string}>;
|
||||||
translateContent?: (args: {
|
translateContent?: (args: {
|
||||||
content: Content; // the content loaded by this plugin instance
|
/** The content loaded by this plugin instance. */
|
||||||
translationFiles: TranslationFiles;
|
content: Content;
|
||||||
|
translationFiles: TranslationFile[];
|
||||||
}) => Content;
|
}) => Content;
|
||||||
translateThemeConfig?: (args: {
|
translateThemeConfig?: (args: {
|
||||||
themeConfig: ThemeConfig;
|
themeConfig: ThemeConfig;
|
||||||
translationFiles: TranslationFiles;
|
translationFiles: TranslationFile[];
|
||||||
}) => ThemeConfig;
|
}) => ThemeConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type InitializedPlugin<Content = unknown> = Plugin<Content> & {
|
export type NormalizedPluginConfig = {
|
||||||
|
/**
|
||||||
|
* The default export of the plugin module, or alternatively, what's provided
|
||||||
|
* in the config file as inline plugins. Note that if a file is like:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* export default plugin() {...}
|
||||||
|
* export validateOptions() {...}
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Then the static methods may not exist here. `pluginModule.module` will
|
||||||
|
* always take priority.
|
||||||
|
*/
|
||||||
|
plugin: PluginModule;
|
||||||
|
/** Options as they are provided in the config, not validated yet. */
|
||||||
|
options: PluginOptions;
|
||||||
|
/** Only available when a string is provided in config. */
|
||||||
|
pluginModule?: {
|
||||||
|
/**
|
||||||
|
* Raw module name as provided in the config. Shorthands have been resolved,
|
||||||
|
* so at least it's directly `require.resolve`able.
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/** Whatever gets imported with `require`. */
|
||||||
|
module: ImportedPluginModule;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Different from `pluginModule.path`, this one is always an absolute path,
|
||||||
|
* used to resolve relative paths returned from lifecycles. If it's an inline
|
||||||
|
* plugin, it will be path to the config file.
|
||||||
|
*/
|
||||||
|
entryPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InitializedPlugin = Plugin & {
|
||||||
readonly options: Required<PluginOptions>;
|
readonly options: Required<PluginOptions>;
|
||||||
readonly version: PluginVersionInformation;
|
readonly version: PluginVersionInformation;
|
||||||
/**
|
/**
|
||||||
|
@ -294,8 +340,8 @@ export type InitializedPlugin<Content = unknown> = Plugin<Content> & {
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoadedPlugin<Content = unknown> = InitializedPlugin<Content> & {
|
export type LoadedPlugin = InitializedPlugin & {
|
||||||
readonly content: Content;
|
readonly content: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SwizzleAction = 'eject' | 'wrap';
|
export type SwizzleAction = 'eject' | 'wrap';
|
||||||
|
@ -314,9 +360,7 @@ export type SwizzleConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginModule = {
|
export type PluginModule = {
|
||||||
<Options, Content>(context: LoadContext, options: Options):
|
(context: LoadContext, options: unknown): Plugin | Promise<Plugin>;
|
||||||
| Plugin<Content>
|
|
||||||
| Promise<Plugin<Content>>;
|
|
||||||
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
|
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
|
||||||
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
|
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
|
||||||
|
|
||||||
|
@ -328,11 +372,11 @@ export type ImportedPluginModule = PluginModule & {
|
||||||
default?: PluginModule;
|
default?: PluginModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
|
export type ConfigureWebpackFn = Plugin['configureWebpack'];
|
||||||
export type ConfigureWebpackFnMergeStrategy = {
|
export type ConfigureWebpackFnMergeStrategy = {
|
||||||
[key: string]: CustomizeRuleString;
|
[key: string]: CustomizeRuleString;
|
||||||
};
|
};
|
||||||
export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
|
export type ConfigurePostCssFn = Plugin['configurePostCss'];
|
||||||
|
|
||||||
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
||||||
|
|
||||||
|
@ -342,10 +386,10 @@ export type PluginConfig =
|
||||||
| [PluginModule, PluginOptions]
|
| [PluginModule, PluginOptions]
|
||||||
| PluginModule;
|
| PluginModule;
|
||||||
|
|
||||||
export interface ChunkRegistry {
|
export type ChunkRegistry = {
|
||||||
loader: string;
|
loader: string;
|
||||||
modulePath: string;
|
modulePath: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Module =
|
export type Module =
|
||||||
| {
|
| {
|
||||||
|
@ -355,15 +399,15 @@ export type Module =
|
||||||
}
|
}
|
||||||
| string;
|
| string;
|
||||||
|
|
||||||
export interface RouteModule {
|
export type RouteModule = {
|
||||||
[module: string]: Module | RouteModule | RouteModule[];
|
[module: string]: Module | RouteModule | RouteModule[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ChunkNames {
|
export type ChunkNames = {
|
||||||
[name: string]: string | null | ChunkNames | ChunkNames[];
|
[name: string]: string | null | ChunkNames | ChunkNames[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface RouteConfig {
|
export type RouteConfig = {
|
||||||
path: string;
|
path: string;
|
||||||
component: string;
|
component: string;
|
||||||
modules?: RouteModule;
|
modules?: RouteModule;
|
||||||
|
@ -371,25 +415,25 @@ export interface RouteConfig {
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
[propName: string]: unknown;
|
[propName: string]: unknown;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface RouteContext {
|
export type RouteContext = {
|
||||||
/**
|
/**
|
||||||
* Plugin-specific context data.
|
* Plugin-specific context data.
|
||||||
*/
|
*/
|
||||||
data?: object | undefined;
|
data?: object | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-level plugin routes automatically add some context data to the route.
|
* Top-level plugin routes automatically add some context data to the route.
|
||||||
* This permits us to know which plugin is handling the current route.
|
* This permits us to know which plugin is handling the current route.
|
||||||
*/
|
*/
|
||||||
export interface PluginRouteContext extends RouteContext {
|
export type PluginRouteContext = RouteContext & {
|
||||||
plugin: {
|
plugin: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
|
@ -398,12 +442,14 @@ export type Route = {
|
||||||
readonly routes?: Route[];
|
readonly routes?: Route[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Aliases used for Webpack resolution (when using docusaurus swizzle)
|
/**
|
||||||
export interface ThemeAliases {
|
* Aliases used for Webpack resolution (useful for implementing swizzling)
|
||||||
|
*/
|
||||||
|
export type ThemeAliases = {
|
||||||
[alias: string]: string;
|
[alias: string]: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ConfigureWebpackUtils {
|
export type ConfigureWebpackUtils = {
|
||||||
getStyleLoaders: (
|
getStyleLoaders: (
|
||||||
isServer: boolean,
|
isServer: boolean,
|
||||||
cssOptions: {
|
cssOptions: {
|
||||||
|
@ -414,23 +460,19 @@ export interface ConfigureWebpackUtils {
|
||||||
isServer: boolean;
|
isServer: boolean;
|
||||||
babelOptions?: {[key: string]: unknown};
|
babelOptions?: {[key: string]: unknown};
|
||||||
}) => RuleSetRule;
|
}) => RuleSetRule;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface HtmlTagObject {
|
type HtmlTagObject = {
|
||||||
/**
|
/**
|
||||||
* Attributes of the html tag
|
* Attributes of the html tag.
|
||||||
* E.g. `{'disabled': true, 'value': 'demo', 'rel': 'preconnect'}`
|
* E.g. `{ disabled: true, value: "demo", rel: "preconnect" }`
|
||||||
*/
|
*/
|
||||||
attributes?: Partial<{[key: string]: string | boolean}>;
|
attributes?: Partial<{[key: string]: string | boolean}>;
|
||||||
/**
|
/** The tag name, e.g. `div`, `script`, `link`, `meta` */
|
||||||
* The tag name e.g. `div`, `script`, `link`, `meta`
|
|
||||||
*/
|
|
||||||
tagName: string;
|
tagName: string;
|
||||||
/**
|
/** The inner HTML */
|
||||||
* The inner HTML
|
|
||||||
*/
|
|
||||||
innerHTML?: string;
|
innerHTML?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ValidationSchema<T> = Joi.ObjectSchema<T>;
|
export type ValidationSchema<T> = Joi.ObjectSchema<T>;
|
||||||
|
|
||||||
|
@ -444,10 +486,10 @@ export type OptionValidationContext<T, U> = {
|
||||||
options: T;
|
options: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ThemeConfigValidationContext<T> {
|
export type ThemeConfigValidationContext<T> = {
|
||||||
validate: Validate<T, T>;
|
validate: Validate<T, T>;
|
||||||
themeConfig: Partial<T>;
|
themeConfig: Partial<T>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TOCItem = {
|
export type TOCItem = {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
|
|
|
@ -74,7 +74,6 @@
|
||||||
"html-tags": "^3.1.0",
|
"html-tags": "^3.1.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"import-fresh": "^3.3.0",
|
"import-fresh": "^3.3.0",
|
||||||
"is-root": "^2.1.0",
|
|
||||||
"leven": "^3.1.0",
|
"leven": "^3.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
|
|
|
@ -61,7 +61,8 @@ export async function build(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const context = await loadContext(siteDir, {
|
const context = await loadContext({
|
||||||
|
siteDir,
|
||||||
customOutDir: cliOptions.outDir,
|
customOutDir: cliOptions.outDir,
|
||||||
customConfigFilePath: cliOptions.config,
|
customConfigFilePath: cliOptions.config,
|
||||||
locale: cliOptions.locale,
|
locale: cliOptions.locale,
|
||||||
|
@ -109,7 +110,8 @@ async function buildLocale({
|
||||||
process.env.NODE_ENV = 'production';
|
process.env.NODE_ENV = 'production';
|
||||||
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
|
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
|
||||||
|
|
||||||
const props: Props = await load(siteDir, {
|
const props: Props = await load({
|
||||||
|
siteDir,
|
||||||
customOutDir: cliOptions.outDir,
|
customOutDir: cliOptions.outDir,
|
||||||
customConfigFilePath: cliOptions.config,
|
customConfigFilePath: cliOptions.config,
|
||||||
locale,
|
locale,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import choosePort from '../choosePort';
|
import {choosePort} from '../server/choosePort';
|
||||||
import type {HostPortCLIOptions} from '@docusaurus/types';
|
import type {HostPortCLIOptions} from '@docusaurus/types';
|
||||||
import {DEFAULT_PORT} from '@docusaurus/utils';
|
import {DEFAULT_PORT} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,8 @@ export async function deploy(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
cliOptions: Partial<BuildCLIOptions> = {},
|
cliOptions: Partial<BuildCLIOptions> = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const {outDir, siteConfig, siteConfigPath} = await loadContext(siteDir, {
|
const {outDir, siteConfig, siteConfigPath} = await loadContext({
|
||||||
|
siteDir,
|
||||||
customConfigFilePath: cliOptions.config,
|
customConfigFilePath: cliOptions.config,
|
||||||
customOutDir: cliOptions.outDir,
|
customOutDir: cliOptions.outDir,
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ export async function externalCommand(
|
||||||
cli: CommanderStatic,
|
cli: CommanderStatic,
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugins = await initPlugins(context);
|
const plugins = await initPlugins(context);
|
||||||
|
|
||||||
// Plugin Lifecycle - extendCli.
|
// Plugin Lifecycle - extendCli.
|
||||||
|
|
|
@ -9,7 +9,7 @@ import http from 'http';
|
||||||
import serveHandler from 'serve-handler';
|
import serveHandler from 'serve-handler';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {loadSiteConfig} from '../server';
|
import {loadSiteConfig} from '../server/config';
|
||||||
import {build} from './build';
|
import {build} from './build';
|
||||||
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
|
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
|
||||||
import type {ServeCLIOptions} from '@docusaurus/types';
|
import type {ServeCLIOptions} from '@docusaurus/types';
|
||||||
|
|
|
@ -37,7 +37,8 @@ export async function start(
|
||||||
logger.info('Starting the development server...');
|
logger.info('Starting the development server...');
|
||||||
|
|
||||||
function loadSite() {
|
function loadSite() {
|
||||||
return load(siteDir, {
|
return load({
|
||||||
|
siteDir,
|
||||||
customConfigFilePath: cliOptions.config,
|
customConfigFilePath: cliOptions.config,
|
||||||
locale: cliOptions.locale,
|
locale: cliOptions.locale,
|
||||||
localizePath: undefined, // should this be configurable?
|
localizePath: undefined, // should this be configurable?
|
||||||
|
|
|
@ -12,8 +12,8 @@ import type {
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
SwizzleAction,
|
SwizzleAction,
|
||||||
SwizzleActionStatus,
|
SwizzleActionStatus,
|
||||||
|
NormalizedPluginConfig,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import type {NormalizedPluginConfig} from '../../server/plugins/init';
|
|
||||||
|
|
||||||
export const SwizzleActions: SwizzleAction[] = ['wrap', 'eject'];
|
export const SwizzleActions: SwizzleAction[] = ['wrap', 'eject'];
|
||||||
|
|
||||||
|
|
|
@ -6,25 +6,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {loadContext} from '../../server';
|
import {loadContext} from '../../server';
|
||||||
import {initPlugins, normalizePluginConfigs} from '../../server/plugins/init';
|
import {initPlugins} from '../../server/plugins/init';
|
||||||
import {loadPluginConfigs} from '../../server/plugins/configs';
|
import {loadPluginConfigs} from '../../server/plugins/configs';
|
||||||
import type {SwizzleContext} from './common';
|
import type {SwizzleContext} from './common';
|
||||||
|
|
||||||
export async function initSwizzleContext(
|
export async function initSwizzleContext(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
): Promise<SwizzleContext> {
|
): Promise<SwizzleContext> {
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugins = await initPlugins(context);
|
const plugins = await initPlugins(context);
|
||||||
const pluginConfigs = await loadPluginConfigs(context);
|
const pluginConfigs = await loadPluginConfigs(context);
|
||||||
|
|
||||||
const pluginsNormalized = await normalizePluginConfigs(
|
|
||||||
pluginConfigs,
|
|
||||||
context.siteConfigPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: plugins.map((plugin, pluginIndex) => ({
|
plugins: plugins.map((plugin, pluginIndex) => ({
|
||||||
plugin: pluginsNormalized[pluginIndex]!,
|
plugin: pluginConfigs[pluginIndex]!,
|
||||||
instance: plugin,
|
instance: plugin,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,7 @@ async function transformMarkdownFile(
|
||||||
* transformed
|
* transformed
|
||||||
*/
|
*/
|
||||||
async function getPathsToWatch(siteDir: string): Promise<string[]> {
|
async function getPathsToWatch(siteDir: string): Promise<string[]> {
|
||||||
const context = await loadContext(siteDir);
|
const context = await loadContext({siteDir});
|
||||||
const plugins = await initPlugins(context);
|
const plugins = await initPlugins(context);
|
||||||
return plugins.flatMap((plugin) => plugin?.getPathsToWatch?.() ?? []);
|
return plugins.flatMap((plugin) => plugin?.getPathsToWatch?.() ?? []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,8 @@ export async function writeTranslations(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
options: WriteTranslationsOptions & ConfigOptions & {locale?: string},
|
options: WriteTranslationsOptions & ConfigOptions & {locale?: string},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const context = await loadContext(siteDir, {
|
const context = await loadContext({
|
||||||
|
siteDir,
|
||||||
customConfigFilePath: options.config,
|
customConfigFilePath: options.config,
|
||||||
locale: options.locale,
|
locale: options.locale,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,161 +1,162 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`loadConfig website with incomplete siteConfig 1`] = `
|
exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"\\"url\\" is required
|
|
||||||
"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`loadConfig website with useless field (wrong field) in siteConfig 1`] = `
|
|
||||||
"These field(s) (\\"useLessField\\",) are not recognized in docusaurus.config.js.
|
|
||||||
If you still want these fields to be in your configuration, put them in the \\"customFields\\" field.
|
|
||||||
See https://docusaurus.io/docs/api/docusaurus-config/#customfields"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`loadConfig website with valid async config 1`] = `
|
|
||||||
{
|
{
|
||||||
"baseUrl": "/",
|
"siteConfig": {
|
||||||
"baseUrlIssueBanner": true,
|
"baseUrl": "/",
|
||||||
"clientModules": [],
|
"baseUrlIssueBanner": true,
|
||||||
"customFields": {},
|
"clientModules": [],
|
||||||
"i18n": {
|
"customFields": {},
|
||||||
"defaultLocale": "en",
|
"i18n": {
|
||||||
"localeConfigs": {},
|
"defaultLocale": "en",
|
||||||
"locales": [
|
"localeConfigs": {},
|
||||||
"en",
|
"locales": [
|
||||||
|
"en",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"noIndex": false,
|
||||||
|
"onBrokenLinks": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
"onDuplicateRoutes": "warn",
|
||||||
|
"organizationName": "endiliey",
|
||||||
|
"plugins": [],
|
||||||
|
"presets": [],
|
||||||
|
"projectName": "hello",
|
||||||
|
"scripts": [],
|
||||||
|
"staticDirectories": [
|
||||||
|
"static",
|
||||||
],
|
],
|
||||||
|
"stylesheets": [],
|
||||||
|
"tagline": "Hello World",
|
||||||
|
"themeConfig": {},
|
||||||
|
"themes": [],
|
||||||
|
"title": "Hello",
|
||||||
|
"titleDelimiter": "|",
|
||||||
|
"url": "https://docusaurus.io",
|
||||||
},
|
},
|
||||||
"noIndex": false,
|
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/configAsync.config.js",
|
||||||
"onBrokenLinks": "throw",
|
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
|
||||||
"organizationName": "endiliey",
|
|
||||||
"plugins": [],
|
|
||||||
"presets": [],
|
|
||||||
"projectName": "hello",
|
|
||||||
"scripts": [],
|
|
||||||
"staticDirectories": [
|
|
||||||
"static",
|
|
||||||
],
|
|
||||||
"stylesheets": [],
|
|
||||||
"tagline": "Hello World",
|
|
||||||
"themeConfig": {},
|
|
||||||
"themes": [],
|
|
||||||
"title": "Hello",
|
|
||||||
"titleDelimiter": "|",
|
|
||||||
"url": "https://docusaurus.io",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadConfig website with valid async config creator function 1`] = `
|
exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
{
|
{
|
||||||
"baseUrl": "/",
|
"siteConfig": {
|
||||||
"baseUrlIssueBanner": true,
|
"baseUrl": "/",
|
||||||
"clientModules": [],
|
"baseUrlIssueBanner": true,
|
||||||
"customFields": {},
|
"clientModules": [],
|
||||||
"i18n": {
|
"customFields": {},
|
||||||
"defaultLocale": "en",
|
"i18n": {
|
||||||
"localeConfigs": {},
|
"defaultLocale": "en",
|
||||||
"locales": [
|
"localeConfigs": {},
|
||||||
"en",
|
"locales": [
|
||||||
|
"en",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"noIndex": false,
|
||||||
|
"onBrokenLinks": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
"onDuplicateRoutes": "warn",
|
||||||
|
"organizationName": "endiliey",
|
||||||
|
"plugins": [],
|
||||||
|
"presets": [],
|
||||||
|
"projectName": "hello",
|
||||||
|
"scripts": [],
|
||||||
|
"staticDirectories": [
|
||||||
|
"static",
|
||||||
],
|
],
|
||||||
|
"stylesheets": [],
|
||||||
|
"tagline": "Hello World",
|
||||||
|
"themeConfig": {},
|
||||||
|
"themes": [],
|
||||||
|
"title": "Hello",
|
||||||
|
"titleDelimiter": "|",
|
||||||
|
"url": "https://docusaurus.io",
|
||||||
},
|
},
|
||||||
"noIndex": false,
|
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/createConfigAsync.config.js",
|
||||||
"onBrokenLinks": "throw",
|
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
|
||||||
"organizationName": "endiliey",
|
|
||||||
"plugins": [],
|
|
||||||
"presets": [],
|
|
||||||
"projectName": "hello",
|
|
||||||
"scripts": [],
|
|
||||||
"staticDirectories": [
|
|
||||||
"static",
|
|
||||||
],
|
|
||||||
"stylesheets": [],
|
|
||||||
"tagline": "Hello World",
|
|
||||||
"themeConfig": {},
|
|
||||||
"themes": [],
|
|
||||||
"title": "Hello",
|
|
||||||
"titleDelimiter": "|",
|
|
||||||
"url": "https://docusaurus.io",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadConfig website with valid config creator function 1`] = `
|
exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
{
|
{
|
||||||
"baseUrl": "/",
|
"siteConfig": {
|
||||||
"baseUrlIssueBanner": true,
|
"baseUrl": "/",
|
||||||
"clientModules": [],
|
"baseUrlIssueBanner": true,
|
||||||
"customFields": {},
|
"clientModules": [],
|
||||||
"i18n": {
|
"customFields": {},
|
||||||
"defaultLocale": "en",
|
"i18n": {
|
||||||
"localeConfigs": {},
|
"defaultLocale": "en",
|
||||||
"locales": [
|
"localeConfigs": {},
|
||||||
"en",
|
"locales": [
|
||||||
|
"en",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"noIndex": false,
|
||||||
|
"onBrokenLinks": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
"onDuplicateRoutes": "warn",
|
||||||
|
"organizationName": "endiliey",
|
||||||
|
"plugins": [],
|
||||||
|
"presets": [],
|
||||||
|
"projectName": "hello",
|
||||||
|
"scripts": [],
|
||||||
|
"staticDirectories": [
|
||||||
|
"static",
|
||||||
],
|
],
|
||||||
|
"stylesheets": [],
|
||||||
|
"tagline": "Hello World",
|
||||||
|
"themeConfig": {},
|
||||||
|
"themes": [],
|
||||||
|
"title": "Hello",
|
||||||
|
"titleDelimiter": "|",
|
||||||
|
"url": "https://docusaurus.io",
|
||||||
},
|
},
|
||||||
"noIndex": false,
|
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/createConfig.config.js",
|
||||||
"onBrokenLinks": "throw",
|
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
|
||||||
"organizationName": "endiliey",
|
|
||||||
"plugins": [],
|
|
||||||
"presets": [],
|
|
||||||
"projectName": "hello",
|
|
||||||
"scripts": [],
|
|
||||||
"staticDirectories": [
|
|
||||||
"static",
|
|
||||||
],
|
|
||||||
"stylesheets": [],
|
|
||||||
"tagline": "Hello World",
|
|
||||||
"themeConfig": {},
|
|
||||||
"themes": [],
|
|
||||||
"title": "Hello",
|
|
||||||
"titleDelimiter": "|",
|
|
||||||
"url": "https://docusaurus.io",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadConfig website with valid siteConfig 1`] = `
|
exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
{
|
{
|
||||||
"baseUrl": "/",
|
"siteConfig": {
|
||||||
"baseUrlIssueBanner": true,
|
"baseUrl": "/",
|
||||||
"clientModules": [],
|
"baseUrlIssueBanner": true,
|
||||||
"customFields": {},
|
"clientModules": [],
|
||||||
"favicon": "img/docusaurus.ico",
|
"customFields": {},
|
||||||
"i18n": {
|
"favicon": "img/docusaurus.ico",
|
||||||
"defaultLocale": "en",
|
"i18n": {
|
||||||
"localeConfigs": {},
|
"defaultLocale": "en",
|
||||||
"locales": [
|
"localeConfigs": {},
|
||||||
"en",
|
"locales": [
|
||||||
|
"en",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"noIndex": false,
|
||||||
|
"onBrokenLinks": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
"onDuplicateRoutes": "warn",
|
||||||
|
"organizationName": "endiliey",
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@docusaurus/plugin-content-docs",
|
||||||
|
{
|
||||||
|
"path": "../docs",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@docusaurus/plugin-content-pages",
|
||||||
],
|
],
|
||||||
|
"presets": [],
|
||||||
|
"projectName": "hello",
|
||||||
|
"scripts": [],
|
||||||
|
"staticDirectories": [
|
||||||
|
"static",
|
||||||
|
],
|
||||||
|
"stylesheets": [],
|
||||||
|
"tagline": "Hello World",
|
||||||
|
"themeConfig": {},
|
||||||
|
"themes": [],
|
||||||
|
"title": "Hello",
|
||||||
|
"titleDelimiter": "|",
|
||||||
|
"url": "https://docusaurus.io",
|
||||||
},
|
},
|
||||||
"noIndex": false,
|
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js",
|
||||||
"onBrokenLinks": "throw",
|
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
|
||||||
"organizationName": "endiliey",
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"@docusaurus/plugin-content-docs",
|
|
||||||
{
|
|
||||||
"path": "../docs",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"@docusaurus/plugin-content-pages",
|
|
||||||
],
|
|
||||||
"presets": [],
|
|
||||||
"projectName": "hello",
|
|
||||||
"scripts": [],
|
|
||||||
"staticDirectories": [
|
|
||||||
"static",
|
|
||||||
],
|
|
||||||
"stylesheets": [],
|
|
||||||
"tagline": "Hello World",
|
|
||||||
"themeConfig": {},
|
|
||||||
"themes": [],
|
|
||||||
"title": "Hello",
|
|
||||||
"titleDelimiter": "|",
|
|
||||||
"url": "https://docusaurus.io",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`handleDuplicateRoutes works 1`] = `
|
|
||||||
"Duplicate routes found!
|
|
||||||
- Attempting to create page at /search, but a page already exists at this route.
|
|
||||||
- Attempting to create page at /sameDoc, but a page already exists at this route.
|
|
||||||
- Attempting to create page at /, but a page already exists at this route.
|
|
||||||
- Attempting to create page at /, but a page already exists at this route.
|
|
||||||
This could lead to non-deterministic routing behavior."
|
|
||||||
`;
|
|
|
@ -1,5 +1,14 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`handleDuplicateRoutes works 1`] = `
|
||||||
|
"Duplicate routes found!
|
||||||
|
- Attempting to create page at /search, but a page already exists at this route.
|
||||||
|
- Attempting to create page at /sameDoc, but a page already exists at this route.
|
||||||
|
- Attempting to create page at /, but a page already exists at this route.
|
||||||
|
- Attempting to create page at /, but a page already exists at this route.
|
||||||
|
This could lead to non-deterministic routing behavior."
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`loadRoutes loads flat route config 1`] = `
|
exports[`loadRoutes loads flat route config 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
|
|
|
@ -6,86 +6,76 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {loadConfig} from '../config';
|
import {loadSiteConfig} from '../config';
|
||||||
|
|
||||||
|
describe('loadSiteConfig', () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__', 'configs');
|
||||||
|
|
||||||
describe('loadConfig', () => {
|
|
||||||
it('website with valid siteConfig', async () => {
|
it('website with valid siteConfig', async () => {
|
||||||
const siteDir = path.join(
|
const config = await loadSiteConfig({
|
||||||
__dirname,
|
siteDir: path.join(__dirname, '__fixtures__', 'simple-site'),
|
||||||
'__fixtures__',
|
});
|
||||||
'simple-site',
|
|
||||||
'docusaurus.config.js',
|
|
||||||
);
|
|
||||||
const config = await loadConfig(siteDir);
|
|
||||||
expect(config).toMatchSnapshot();
|
expect(config).toMatchSnapshot();
|
||||||
expect(config).not.toEqual({});
|
expect(config).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with valid config creator function', async () => {
|
it('website with valid config creator function', async () => {
|
||||||
const siteDir = path.join(
|
const config = await loadSiteConfig({
|
||||||
__dirname,
|
siteDir,
|
||||||
'__fixtures__',
|
customConfigFilePath: 'createConfig.config.js',
|
||||||
'configs',
|
});
|
||||||
'createConfig.config.js',
|
|
||||||
);
|
|
||||||
const config = await loadConfig(siteDir);
|
|
||||||
expect(config).toMatchSnapshot();
|
expect(config).toMatchSnapshot();
|
||||||
expect(config).not.toEqual({});
|
expect(config).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with valid async config', async () => {
|
it('website with valid async config', async () => {
|
||||||
const siteDir = path.join(
|
const config = await loadSiteConfig({
|
||||||
__dirname,
|
siteDir,
|
||||||
'__fixtures__',
|
customConfigFilePath: 'configAsync.config.js',
|
||||||
'configs',
|
});
|
||||||
'configAsync.config.js',
|
|
||||||
);
|
|
||||||
const config = await loadConfig(siteDir);
|
|
||||||
expect(config).toMatchSnapshot();
|
expect(config).toMatchSnapshot();
|
||||||
expect(config).not.toEqual({});
|
expect(config).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with valid async config creator function', async () => {
|
it('website with valid async config creator function', async () => {
|
||||||
const siteDir = path.join(
|
const config = await loadSiteConfig({
|
||||||
__dirname,
|
siteDir,
|
||||||
'__fixtures__',
|
customConfigFilePath: 'createConfigAsync.config.js',
|
||||||
'configs',
|
});
|
||||||
'createConfigAsync.config.js',
|
|
||||||
);
|
|
||||||
const config = await loadConfig(siteDir);
|
|
||||||
expect(config).toMatchSnapshot();
|
expect(config).toMatchSnapshot();
|
||||||
expect(config).not.toEqual({});
|
expect(config).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with incomplete siteConfig', async () => {
|
it('website with incomplete siteConfig', async () => {
|
||||||
const siteDir = path.join(
|
await expect(
|
||||||
__dirname,
|
loadSiteConfig({
|
||||||
'__fixtures__',
|
siteDir: path.join(__dirname, '__fixtures__', 'bad-site'),
|
||||||
'bad-site',
|
}),
|
||||||
'docusaurus.config.js',
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
);
|
"\\"url\\" is required
|
||||||
await expect(loadConfig(siteDir)).rejects.toThrowErrorMatchingSnapshot();
|
"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with useless field (wrong field) in siteConfig', async () => {
|
it('website with useless field (wrong field) in siteConfig', async () => {
|
||||||
const siteDir = path.join(
|
await expect(
|
||||||
__dirname,
|
loadSiteConfig({
|
||||||
'__fixtures__',
|
siteDir: path.join(__dirname, '__fixtures__', 'wrong-site'),
|
||||||
'wrong-site',
|
}),
|
||||||
'docusaurus.config.js',
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
);
|
"These field(s) (\\"useLessField\\",) are not recognized in docusaurus.config.js.
|
||||||
await expect(loadConfig(siteDir)).rejects.toThrowErrorMatchingSnapshot();
|
If you still want these fields to be in your configuration, put them in the \\"customFields\\" field.
|
||||||
|
See https://docusaurus.io/docs/api/docusaurus-config/#customfields"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('website with no siteConfig', async () => {
|
it('website with no siteConfig', async () => {
|
||||||
const siteDir = path.join(
|
await expect(
|
||||||
__dirname,
|
loadSiteConfig({
|
||||||
'__fixtures__',
|
siteDir: path.join(__dirname, '__fixtures__', 'nonExisting'),
|
||||||
'nonExisting',
|
}),
|
||||||
'docusaurus.config.js',
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
);
|
`"Config file at \\"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/nonExisting/docusaurus.config.js\\" not found."`,
|
||||||
await expect(loadConfig(siteDir)).rejects.toThrowError(
|
|
||||||
/Config file at ".*?__fixtures__[/\\]nonExisting[/\\]docusaurus.config.js" not found.$/,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,53 +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 {jest} from '@jest/globals';
|
|
||||||
import {handleDuplicateRoutes} from '../duplicateRoutes';
|
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
|
||||||
|
|
||||||
const routes: RouteConfig[] = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: '/search', component: ''},
|
|
||||||
{path: '/sameDoc', component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: '/search', component: ''},
|
|
||||||
{path: '/sameDoc', component: ''},
|
|
||||||
{path: '/uniqueDoc', component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('handleDuplicateRoutes', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(() => {
|
|
||||||
handleDuplicateRoutes(routes, 'throw');
|
|
||||||
}).toThrowErrorMatchingSnapshot();
|
|
||||||
const consoleMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
handleDuplicateRoutes(routes, 'ignore');
|
|
||||||
expect(consoleMock).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -5,9 +5,52 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {loadRoutes} from '../routes';
|
import {jest} from '@jest/globals';
|
||||||
|
import {loadRoutes, handleDuplicateRoutes} from '../routes';
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
describe('handleDuplicateRoutes', () => {
|
||||||
|
const routes: RouteConfig[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: '/search', component: ''},
|
||||||
|
{path: '/sameDoc', component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: '/search', component: ''},
|
||||||
|
{path: '/sameDoc', component: ''},
|
||||||
|
{path: '/uniqueDoc', component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
it('works', () => {
|
||||||
|
expect(() => {
|
||||||
|
handleDuplicateRoutes(routes, 'throw');
|
||||||
|
}).toThrowErrorMatchingSnapshot();
|
||||||
|
const consoleMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
handleDuplicateRoutes(routes, 'ignore');
|
||||||
|
expect(consoleMock).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadRoutes', () => {
|
describe('loadRoutes', () => {
|
||||||
it('loads nested route config', async () => {
|
it('loads nested route config', async () => {
|
||||||
const nestedRouteConfig: RouteConfig = {
|
const nestedRouteConfig: RouteConfig = {
|
||||||
|
@ -44,7 +87,7 @@ describe('loadRoutes', () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
await expect(
|
await expect(
|
||||||
loadRoutes([nestedRouteConfig], '/'),
|
loadRoutes([nestedRouteConfig], '/', 'ignore'),
|
||||||
).resolves.toMatchSnapshot();
|
).resolves.toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,7 +122,9 @@ describe('loadRoutes', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await expect(loadRoutes([flatRouteConfig], '/')).resolves.toMatchSnapshot();
|
await expect(
|
||||||
|
loadRoutes([flatRouteConfig], '/', 'ignore'),
|
||||||
|
).resolves.toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid route config', async () => {
|
it('rejects invalid route config', async () => {
|
||||||
|
@ -87,7 +132,7 @@ describe('loadRoutes', () => {
|
||||||
component: 'hello/world.js',
|
component: 'hello/world.js',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(loadRoutes([routeConfigWithoutPath], '/')).rejects
|
await expect(loadRoutes([routeConfigWithoutPath], '/', 'ignore')).rejects
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Invalid route config: path must be a string and component is required.
|
"Invalid route config: path must be a string and component is required.
|
||||||
{\\"component\\":\\"hello/world.js\\"}"
|
{\\"component\\":\\"hello/world.js\\"}"
|
||||||
|
@ -97,8 +142,8 @@ describe('loadRoutes', () => {
|
||||||
path: '/hello/world',
|
path: '/hello/world',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(loadRoutes([routeConfigWithoutComponent], '/')).rejects
|
await expect(loadRoutes([routeConfigWithoutComponent], '/', 'ignore'))
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
.rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Invalid route config: path must be a string and component is required.
|
"Invalid route config: path must be a string and component is required.
|
||||||
{\\"path\\":\\"/hello/world\\"}"
|
{\\"path\\":\\"/hello/world\\"}"
|
||||||
`);
|
`);
|
||||||
|
@ -110,6 +155,8 @@ describe('loadRoutes', () => {
|
||||||
component: 'hello/world.js',
|
component: 'hello/world.js',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(loadRoutes([routeConfig], '/')).resolves.toMatchSnapshot();
|
await expect(
|
||||||
|
loadRoutes([routeConfig], '/', 'ignore'),
|
||||||
|
).resolves.toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,9 +17,9 @@ export default async function loadSetup(name: string): Promise<Props> {
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return load(customSite);
|
return load({siteDir: customSite});
|
||||||
case 'simple':
|
case 'simple':
|
||||||
default:
|
default:
|
||||||
return load(simpleSite);
|
return load({siteDir: simpleSite});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,11 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* This feature was heavily inspired by create-react-app and
|
|
||||||
* uses many of the same utility functions to implement it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {execSync} from 'child_process';
|
import {execSync} from 'child_process';
|
||||||
import detect from 'detect-port';
|
import detect from 'detect-port';
|
||||||
import isRoot from 'is-root';
|
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import prompts from 'prompts';
|
import prompts from 'prompts';
|
||||||
|
|
||||||
const isInteractive = process.stdout.isTTY;
|
|
||||||
|
|
||||||
const execOptions = {
|
const execOptions = {
|
||||||
encoding: 'utf8' as const,
|
encoding: 'utf8' as const,
|
||||||
stdio: [
|
stdio: [
|
||||||
|
@ -72,10 +64,11 @@ function getProcessForPort(port: number): string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects if program is running on port and prompts user
|
* Detects if program is running on port, and prompts user to choose another if
|
||||||
* to choose another if port is already being used
|
* port is already being used. This feature was heavily inspired by
|
||||||
|
* create-react-app and uses many of the same utility functions to implement it.
|
||||||
*/
|
*/
|
||||||
export default async function choosePort(
|
export async function choosePort(
|
||||||
host: string,
|
host: string,
|
||||||
defaultPort: number,
|
defaultPort: number,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
|
@ -84,8 +77,10 @@ export default async function choosePort(
|
||||||
if (port === defaultPort) {
|
if (port === defaultPort) {
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
const isRoot = process.getuid?.() === 0;
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
const message =
|
const message =
|
||||||
process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
|
process.platform !== 'win32' && defaultPort < 1024 && !isRoot
|
||||||
? `Admin permissions are required to run a server on a port below 1024.`
|
? `Admin permissions are required to run a server on a port below 1024.`
|
||||||
: `Something is already running on port ${defaultPort}.`;
|
: `Something is already running on port ${defaultPort}.`;
|
||||||
if (!isInteractive) {
|
if (!isInteractive) {
|
|
@ -8,7 +8,11 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {LoadedPlugin} from '@docusaurus/types';
|
import type {LoadedPlugin} from '@docusaurus/types';
|
||||||
|
|
||||||
export function loadClientModules(plugins: LoadedPlugin<unknown>[]): string[] {
|
/**
|
||||||
|
* Runs the `getClientModules` lifecycle. The returned file paths are all
|
||||||
|
* absolute.
|
||||||
|
*/
|
||||||
|
export function loadClientModules(plugins: LoadedPlugin[]): string[] {
|
||||||
return plugins.flatMap(
|
return plugins.flatMap(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ??
|
plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ??
|
||||||
|
|
|
@ -5,28 +5,36 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import importFresh from 'import-fresh';
|
import importFresh from 'import-fresh';
|
||||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
import {DEFAULT_CONFIG_FILE_NAME} from '@docusaurus/utils';
|
||||||
|
import type {LoadContext} from '@docusaurus/types';
|
||||||
import {validateConfig} from './configValidation';
|
import {validateConfig} from './configValidation';
|
||||||
|
|
||||||
export async function loadConfig(
|
export async function loadSiteConfig({
|
||||||
configPath: string,
|
siteDir,
|
||||||
): Promise<DocusaurusConfig> {
|
customConfigFilePath,
|
||||||
if (!(await fs.pathExists(configPath))) {
|
}: {
|
||||||
throw new Error(`Config file at "${configPath}" not found.`);
|
siteDir: string;
|
||||||
|
customConfigFilePath?: string;
|
||||||
|
}): Promise<Pick<LoadContext, 'siteConfig' | 'siteConfigPath'>> {
|
||||||
|
const siteConfigPath = path.resolve(
|
||||||
|
siteDir,
|
||||||
|
customConfigFilePath ?? DEFAULT_CONFIG_FILE_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(siteConfigPath))) {
|
||||||
|
throw new Error(`Config file at "${siteConfigPath}" not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const importedConfig = importFresh(configPath) as
|
const importedConfig = importFresh(siteConfigPath);
|
||||||
| Partial<DocusaurusConfig>
|
|
||||||
| Promise<Partial<DocusaurusConfig>>
|
|
||||||
| (() => Partial<DocusaurusConfig>)
|
|
||||||
| (() => Promise<Partial<DocusaurusConfig>>);
|
|
||||||
|
|
||||||
const loadedConfig =
|
const loadedConfig =
|
||||||
typeof importedConfig === 'function'
|
typeof importedConfig === 'function'
|
||||||
? await importedConfig()
|
? await importedConfig()
|
||||||
: await importedConfig;
|
: await importedConfig;
|
||||||
|
|
||||||
return validateConfig(loadedConfig);
|
const siteConfig = validateConfig(loadedConfig);
|
||||||
|
return {siteConfig, siteConfigPath};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +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 {ReportingSeverity, RouteConfig} from '@docusaurus/types';
|
|
||||||
import {reportMessage} from '@docusaurus/utils';
|
|
||||||
import {getAllFinalRoutes} from './utils';
|
|
||||||
|
|
||||||
function getAllDuplicateRoutes(pluginsRouteConfigs: RouteConfig[]): string[] {
|
|
||||||
const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map(
|
|
||||||
(routeConfig) => routeConfig.path,
|
|
||||||
);
|
|
||||||
const seenRoutes = new Set<string>();
|
|
||||||
return allRoutes.filter((route) => {
|
|
||||||
if (seenRoutes.has(route)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
seenRoutes.add(route);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDuplicateRoutesMessage(allDuplicateRoutes: string[]): string {
|
|
||||||
const message = allDuplicateRoutes
|
|
||||||
.map(
|
|
||||||
(duplicateRoute) =>
|
|
||||||
`- Attempting to create page at ${duplicateRoute}, but a page already exists at this route.`,
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleDuplicateRoutes(
|
|
||||||
pluginsRouteConfigs: RouteConfig[],
|
|
||||||
onDuplicateRoutes: ReportingSeverity,
|
|
||||||
): void {
|
|
||||||
if (onDuplicateRoutes === 'ignore') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicatePaths: string[] = getAllDuplicateRoutes(pluginsRouteConfigs);
|
|
||||||
const message: string = getDuplicateRoutesMessage(duplicatePaths);
|
|
||||||
if (message) {
|
|
||||||
const finalMessage = `Duplicate routes found!
|
|
||||||
${message}
|
|
||||||
This could lead to non-deterministic routing behavior.`;
|
|
||||||
reportMessage(finalMessage, onDuplicateRoutes);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ import voidHtmlTags from 'html-tags/void';
|
||||||
import escapeHTML from 'escape-html';
|
import escapeHTML from 'escape-html';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import type {
|
import type {
|
||||||
InjectedHtmlTags,
|
Props,
|
||||||
HtmlTagObject,
|
HtmlTagObject,
|
||||||
HtmlTags,
|
HtmlTags,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
|
@ -62,7 +62,13 @@ function createHtmlTagsString(tags: HtmlTags | undefined): string {
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadHtmlTags(plugins: LoadedPlugin[]): InjectedHtmlTags {
|
/**
|
||||||
|
* Runs the `injectHtmlTags` lifecycle, and aggregates all plugins' tags into
|
||||||
|
* directly render-able HTML markup.
|
||||||
|
*/
|
||||||
|
export function loadHtmlTags(
|
||||||
|
plugins: LoadedPlugin[],
|
||||||
|
): Pick<Props, 'headTags' | 'preBodyTags' | 'postBodyTags'> {
|
||||||
const pluginHtmlTags = plugins.map(
|
const pluginHtmlTags = plugins.map(
|
||||||
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
|
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import {getLangDir} from 'rtl-detect';
|
import {getLangDir} from 'rtl-detect';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
|
import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
|
||||||
|
import type {LoadContextOptions} from './index';
|
||||||
|
|
||||||
function getDefaultLocaleLabel(locale: string) {
|
function getDefaultLocaleLabel(locale: string) {
|
||||||
const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of(
|
const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of(
|
||||||
|
@ -28,7 +29,7 @@ export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
|
||||||
|
|
||||||
export async function loadI18n(
|
export async function loadI18n(
|
||||||
config: DocusaurusConfig,
|
config: DocusaurusConfig,
|
||||||
options: {locale?: string},
|
options: Pick<LoadContextOptions, 'locale'>,
|
||||||
): Promise<I18n> {
|
): Promise<I18n> {
|
||||||
const {i18n: i18nConfig} = config;
|
const {i18n: i18nConfig} = config;
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,13 @@ import {
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import {loadSiteConfig} from './config';
|
||||||
import ssrDefaultTemplate from '../webpack/templates/ssr.html.template';
|
import ssrDefaultTemplate from '../webpack/templates/ssr.html.template';
|
||||||
import {loadClientModules} from './clientModules';
|
import {loadClientModules} from './clientModules';
|
||||||
import {loadConfig} from './config';
|
|
||||||
import {loadPlugins} from './plugins';
|
import {loadPlugins} from './plugins';
|
||||||
import {loadRoutes} from './routes';
|
import {loadRoutes} from './routes';
|
||||||
import {loadHtmlTags} from './htmlTags';
|
import {loadHtmlTags} from './htmlTags';
|
||||||
import {loadSiteMetadata} from './siteMetadata';
|
import {loadSiteMetadata} from './siteMetadata';
|
||||||
import {handleDuplicateRoutes} from './duplicateRoutes';
|
|
||||||
import {loadI18n} from './i18n';
|
import {loadI18n} from './i18n';
|
||||||
import {
|
import {
|
||||||
readCodeTranslationFileContent,
|
readCodeTranslationFileContent,
|
||||||
|
@ -31,45 +30,39 @@ import {
|
||||||
import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types';
|
import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types';
|
||||||
|
|
||||||
export type LoadContextOptions = {
|
export type LoadContextOptions = {
|
||||||
|
/** Usually the CWD; can be overridden with command argument. */
|
||||||
|
siteDir: string;
|
||||||
|
/** Can be customized with `--out-dir` option */
|
||||||
customOutDir?: string;
|
customOutDir?: string;
|
||||||
|
/** Can be customized with `--config` option */
|
||||||
customConfigFilePath?: string;
|
customConfigFilePath?: string;
|
||||||
|
/** Default is `i18n.defaultLocale` */
|
||||||
locale?: string;
|
locale?: string;
|
||||||
localizePath?: boolean; // undefined = only non-default locales paths are localized
|
/**
|
||||||
|
* `true` means the paths will have the locale prepended; `false` means they
|
||||||
|
* won't (useful for `yarn build -l zh-Hans` where the output should be
|
||||||
|
* emitted into `build/` instead of `build/zh-Hans/`); `undefined` is like the
|
||||||
|
* "smart" option where only non-default locale paths are localized
|
||||||
|
*/
|
||||||
|
localizePath?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadSiteConfig({
|
/**
|
||||||
siteDir,
|
* Loading context is the very first step in site building. Its options are
|
||||||
customConfigFilePath,
|
* directly acquired from CLI options. It mainly loads `siteConfig` and the i18n
|
||||||
}: {
|
* context (which includes code translations). The `LoadContext` will be passed
|
||||||
siteDir: string;
|
* to plugin constructors.
|
||||||
customConfigFilePath?: string;
|
*/
|
||||||
}): Promise<{siteConfig: DocusaurusConfig; siteConfigPath: string}> {
|
|
||||||
const siteConfigPath = path.resolve(
|
|
||||||
siteDir,
|
|
||||||
customConfigFilePath ?? DEFAULT_CONFIG_FILE_NAME,
|
|
||||||
);
|
|
||||||
|
|
||||||
const siteConfig = await loadConfig(siteConfigPath);
|
|
||||||
return {siteConfig, siteConfigPath};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadContext(
|
export async function loadContext(
|
||||||
siteDir: string,
|
options: LoadContextOptions,
|
||||||
options: LoadContextOptions = {},
|
|
||||||
): Promise<LoadContext> {
|
): Promise<LoadContext> {
|
||||||
const {customOutDir, locale, customConfigFilePath} = options;
|
const {siteDir, customOutDir, locale, customConfigFilePath} = options;
|
||||||
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
|
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
|
||||||
|
|
||||||
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({
|
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({
|
||||||
siteDir,
|
siteDir,
|
||||||
customConfigFilePath,
|
customConfigFilePath,
|
||||||
});
|
});
|
||||||
const {ssrTemplate} = initialSiteConfig;
|
|
||||||
|
|
||||||
const baseOutDir = path.resolve(
|
|
||||||
siteDir,
|
|
||||||
customOutDir ?? DEFAULT_BUILD_DIR_NAME,
|
|
||||||
);
|
|
||||||
|
|
||||||
const i18n = await loadI18n(initialSiteConfig, {locale});
|
const i18n = await loadI18n(initialSiteConfig, {locale});
|
||||||
|
|
||||||
|
@ -80,7 +73,7 @@ export async function loadContext(
|
||||||
pathType: 'url',
|
pathType: 'url',
|
||||||
});
|
});
|
||||||
const outDir = localizePath({
|
const outDir = localizePath({
|
||||||
path: baseOutDir,
|
path: path.resolve(siteDir, customOutDir ?? DEFAULT_BUILD_DIR_NAME),
|
||||||
i18n,
|
i18n,
|
||||||
options,
|
options,
|
||||||
pathType: 'fs',
|
pathType: 'fs',
|
||||||
|
@ -106,19 +99,22 @@ export async function loadContext(
|
||||||
siteConfig,
|
siteConfig,
|
||||||
siteConfigPath,
|
siteConfigPath,
|
||||||
outDir,
|
outDir,
|
||||||
baseUrl, // TODO to remove: useless, there's already siteConfig.baseUrl! (and yes, it's the same value, cf code above)
|
baseUrl,
|
||||||
i18n,
|
i18n,
|
||||||
ssrTemplate: ssrTemplate ?? ssrDefaultTemplate,
|
ssrTemplate: siteConfig.ssrTemplate ?? ssrDefaultTemplate,
|
||||||
codeTranslations,
|
codeTranslations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load(
|
/**
|
||||||
siteDir: string,
|
* This is the crux of the Docusaurus server-side. It reads everything it needs—
|
||||||
options: LoadContextOptions = {},
|
* code translations, config file, plugin modules... Plugins then use their
|
||||||
): Promise<Props> {
|
* lifecycles to generate content and other data. It is side-effect-ful because
|
||||||
// Context.
|
* it generates temp files in the `.docusaurus` folder for the bundler.
|
||||||
const context: LoadContext = await loadContext(siteDir, options);
|
*/
|
||||||
|
export async function load(options: LoadContextOptions): Promise<Props> {
|
||||||
|
const {siteDir} = options;
|
||||||
|
const context = await loadContext(options);
|
||||||
const {
|
const {
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
|
@ -127,16 +123,28 @@ export async function load(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
i18n,
|
i18n,
|
||||||
ssrTemplate,
|
ssrTemplate,
|
||||||
codeTranslations,
|
codeTranslations: siteCodeTranslations,
|
||||||
} = context;
|
} = context;
|
||||||
// Plugins.
|
|
||||||
const {plugins, pluginsRouteConfigs, globalData, themeConfigTranslated} =
|
const {plugins, pluginsRouteConfigs, globalData, themeConfigTranslated} =
|
||||||
await loadPlugins(context);
|
await loadPlugins(context);
|
||||||
|
|
||||||
// Side-effect to replace the untranslated themeConfig by the translated one
|
// Side-effect to replace the untranslated themeConfig by the translated one
|
||||||
context.siteConfig.themeConfig = themeConfigTranslated;
|
context.siteConfig.themeConfig = themeConfigTranslated;
|
||||||
|
const clientModules = loadClientModules(plugins);
|
||||||
|
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
||||||
|
const {registry, routesChunkNames, routesConfig, routesPaths} =
|
||||||
|
await loadRoutes(
|
||||||
|
pluginsRouteConfigs,
|
||||||
|
baseUrl,
|
||||||
|
siteConfig.onDuplicateRoutes,
|
||||||
|
);
|
||||||
|
const codeTranslations: {[msgId: string]: string} = {
|
||||||
|
...(await getPluginsDefaultCodeTranslationMessages(plugins)),
|
||||||
|
...siteCodeTranslations,
|
||||||
|
};
|
||||||
|
const siteMetadata = await loadSiteMetadata({plugins, siteDir});
|
||||||
|
|
||||||
|
// === Side-effects part ===
|
||||||
|
|
||||||
handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes);
|
|
||||||
const genWarning = generate(
|
const genWarning = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'DONT-EDIT-THIS-FOLDER',
|
'DONT-EDIT-THIS-FOLDER',
|
||||||
|
@ -162,8 +170,6 @@ export default ${JSON.stringify(siteConfig, null, 2)};
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load client modules.
|
|
||||||
const clientModules = loadClientModules(plugins);
|
|
||||||
const genClientModules = generate(
|
const genClientModules = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'client-modules.js',
|
'client-modules.js',
|
||||||
|
@ -177,13 +183,6 @@ ${clientModules
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load extra head & body html tags.
|
|
||||||
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
|
||||||
|
|
||||||
// Routing.
|
|
||||||
const {registry, routesChunkNames, routesConfig, routesPaths} =
|
|
||||||
await loadRoutes(pluginsRouteConfigs, baseUrl);
|
|
||||||
|
|
||||||
const genRegistry = generate(
|
const genRegistry = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'registry.js',
|
'registry.js',
|
||||||
|
@ -220,19 +219,12 @@ ${Object.entries(registry)
|
||||||
JSON.stringify(i18n, null, 2),
|
JSON.stringify(i18n, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
const codeTranslationsWithFallbacks: {[msgId: string]: string} = {
|
|
||||||
...(await getPluginsDefaultCodeTranslationMessages(plugins)),
|
|
||||||
...codeTranslations,
|
|
||||||
};
|
|
||||||
|
|
||||||
const genCodeTranslations = generate(
|
const genCodeTranslations = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'codeTranslations.json',
|
'codeTranslations.json',
|
||||||
JSON.stringify(codeTranslationsWithFallbacks, null, 2),
|
JSON.stringify(codeTranslations, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Version metadata.
|
|
||||||
const siteMetadata = await loadSiteMetadata({plugins, siteDir});
|
|
||||||
const genSiteMetadata = generate(
|
const genSiteMetadata = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
'site-metadata.json',
|
'site-metadata.json',
|
||||||
|
@ -252,7 +244,7 @@ ${Object.entries(registry)
|
||||||
genCodeTranslations,
|
genCodeTranslations,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props: Props = {
|
return {
|
||||||
siteConfig,
|
siteConfig,
|
||||||
siteConfigPath,
|
siteConfigPath,
|
||||||
siteMetadata,
|
siteMetadata,
|
||||||
|
@ -270,6 +262,4 @@ ${Object.entries(registry)
|
||||||
ssrTemplate,
|
ssrTemplate,
|
||||||
codeTranslations,
|
codeTranslations,
|
||||||
};
|
};
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {loadContext, type LoadContextOptions} from '../../index';
|
||||||
import {initPlugins} from '../init';
|
import {initPlugins} from '../init';
|
||||||
|
|
||||||
describe('initPlugins', () => {
|
describe('initPlugins', () => {
|
||||||
async function loadSite(options: LoadContextOptions = {}) {
|
async function loadSite(options: Omit<LoadContextOptions, 'siteDir'> = {}) {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-plugin');
|
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-plugin');
|
||||||
const context = await loadContext(siteDir, options);
|
const context = await loadContext({...options, siteDir});
|
||||||
const plugins = await initPlugins(context);
|
const plugins = await initPlugins(context);
|
||||||
|
|
||||||
return {siteDir, context, plugins};
|
return {siteDir, context, plugins};
|
||||||
|
|
|
@ -6,28 +6,97 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {createRequire} from 'module';
|
import {createRequire} from 'module';
|
||||||
|
import importFresh from 'import-fresh';
|
||||||
import {loadPresets} from './presets';
|
import {loadPresets} from './presets';
|
||||||
import {resolveModuleName} from '../moduleShorthand';
|
import {resolveModuleName} from './moduleShorthand';
|
||||||
import type {LoadContext, PluginConfig} from '@docusaurus/types';
|
import type {
|
||||||
|
LoadContext,
|
||||||
|
PluginConfig,
|
||||||
|
ImportedPluginModule,
|
||||||
|
NormalizedPluginConfig,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
|
async function normalizePluginConfig(
|
||||||
|
pluginConfig: PluginConfig,
|
||||||
|
configPath: string,
|
||||||
|
pluginRequire: NodeRequire,
|
||||||
|
): Promise<NormalizedPluginConfig> {
|
||||||
|
// plugins: ["./plugin"]
|
||||||
|
if (typeof pluginConfig === 'string') {
|
||||||
|
const pluginModuleImport = pluginConfig;
|
||||||
|
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
||||||
|
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
||||||
|
return {
|
||||||
|
plugin: pluginModule?.default ?? pluginModule,
|
||||||
|
options: {},
|
||||||
|
pluginModule: {
|
||||||
|
path: pluginModuleImport,
|
||||||
|
module: pluginModule,
|
||||||
|
},
|
||||||
|
entryPath: pluginPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugins: [() => {...}]
|
||||||
|
if (typeof pluginConfig === 'function') {
|
||||||
|
return {
|
||||||
|
plugin: pluginConfig,
|
||||||
|
options: {},
|
||||||
|
entryPath: configPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugins: [
|
||||||
|
// ["./plugin",options],
|
||||||
|
// ]
|
||||||
|
if (typeof pluginConfig[0] === 'string') {
|
||||||
|
const pluginModuleImport = pluginConfig[0];
|
||||||
|
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
||||||
|
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
||||||
|
return {
|
||||||
|
plugin: pluginModule?.default ?? pluginModule,
|
||||||
|
options: pluginConfig[1],
|
||||||
|
pluginModule: {
|
||||||
|
path: pluginModuleImport,
|
||||||
|
module: pluginModule,
|
||||||
|
},
|
||||||
|
entryPath: pluginPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// plugins: [
|
||||||
|
// [() => {...}, options],
|
||||||
|
// ]
|
||||||
|
return {
|
||||||
|
plugin: pluginConfig[0],
|
||||||
|
options: pluginConfig[1],
|
||||||
|
entryPath: configPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the site config's `presets`, `themes`, and `plugins`, imports them, and
|
||||||
|
* normalizes the return value. Plugin configs are ordered, mostly for theme
|
||||||
|
* alias shadowing. Site themes have the highest priority, and preset plugins
|
||||||
|
* are the lowest.
|
||||||
|
*/
|
||||||
export async function loadPluginConfigs(
|
export async function loadPluginConfigs(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
): Promise<PluginConfig[]> {
|
): Promise<NormalizedPluginConfig[]> {
|
||||||
const preset = await loadPresets(context);
|
const preset = await loadPresets(context);
|
||||||
const {siteConfig, siteConfigPath} = context;
|
const {siteConfig, siteConfigPath} = context;
|
||||||
const require = createRequire(siteConfigPath);
|
const pluginRequire = createRequire(siteConfigPath);
|
||||||
function normalizeShorthand(
|
function normalizeShorthand(
|
||||||
pluginConfig: PluginConfig,
|
pluginConfig: PluginConfig,
|
||||||
pluginType: 'plugin' | 'theme',
|
pluginType: 'plugin' | 'theme',
|
||||||
): PluginConfig {
|
): PluginConfig {
|
||||||
if (typeof pluginConfig === 'string') {
|
if (typeof pluginConfig === 'string') {
|
||||||
return resolveModuleName(pluginConfig, require, pluginType);
|
return resolveModuleName(pluginConfig, pluginRequire, pluginType);
|
||||||
} else if (
|
} else if (
|
||||||
Array.isArray(pluginConfig) &&
|
Array.isArray(pluginConfig) &&
|
||||||
typeof pluginConfig[0] === 'string'
|
typeof pluginConfig[0] === 'string'
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
resolveModuleName(pluginConfig[0], require, pluginType),
|
resolveModuleName(pluginConfig[0], pluginRequire, pluginType),
|
||||||
pluginConfig[1] ?? {},
|
pluginConfig[1] ?? {},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -45,11 +114,20 @@ export async function loadPluginConfigs(
|
||||||
const standaloneThemes = siteConfig.themes.map((theme) =>
|
const standaloneThemes = siteConfig.themes.map((theme) =>
|
||||||
normalizeShorthand(theme, 'theme'),
|
normalizeShorthand(theme, 'theme'),
|
||||||
);
|
);
|
||||||
return [
|
const pluginConfigs = [
|
||||||
...preset.plugins,
|
...preset.plugins,
|
||||||
...preset.themes,
|
...preset.themes,
|
||||||
// Site config should be the highest priority.
|
// Site config should be the highest priority.
|
||||||
...standalonePlugins,
|
...standalonePlugins,
|
||||||
...standaloneThemes,
|
...standaloneThemes,
|
||||||
];
|
];
|
||||||
|
return Promise.all(
|
||||||
|
pluginConfigs.map((pluginConfig) =>
|
||||||
|
normalizePluginConfig(
|
||||||
|
pluginConfig,
|
||||||
|
context.siteConfigPath,
|
||||||
|
pluginRequire,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
AllContent,
|
AllContent,
|
||||||
GlobalData,
|
GlobalData,
|
||||||
TranslationFiles,
|
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
|
@ -27,6 +26,10 @@ import _ from 'lodash';
|
||||||
import {localizePluginTranslationFile} from '../translations/translations';
|
import {localizePluginTranslationFile} from '../translations/translations';
|
||||||
import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
|
import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the plugins, runs `loadContent`, `translateContent`,
|
||||||
|
* `contentLoaded`, and `translateThemeConfig`.
|
||||||
|
*/
|
||||||
export async function loadPlugins(context: LoadContext): Promise<{
|
export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
pluginsRouteConfigs: RouteConfig[];
|
pluginsRouteConfigs: RouteConfig[];
|
||||||
|
@ -52,32 +55,28 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
type ContentLoadedTranslatedPlugin = LoadedPlugin & {
|
const contentLoadedTranslatedPlugins = await Promise.all(
|
||||||
translationFiles: TranslationFiles;
|
loadedPlugins.map(async (plugin) => {
|
||||||
};
|
const translationFiles =
|
||||||
const contentLoadedTranslatedPlugins: ContentLoadedTranslatedPlugin[] =
|
(await plugin?.getTranslationFiles?.({
|
||||||
await Promise.all(
|
content: plugin.content,
|
||||||
loadedPlugins.map(async (contentLoadedPlugin) => {
|
})) ?? [];
|
||||||
const translationFiles =
|
const localizedTranslationFiles = await Promise.all(
|
||||||
(await contentLoadedPlugin?.getTranslationFiles?.({
|
translationFiles.map((translationFile) =>
|
||||||
content: contentLoadedPlugin.content,
|
localizePluginTranslationFile({
|
||||||
})) ?? [];
|
locale: context.i18n.currentLocale,
|
||||||
const localizedTranslationFiles = await Promise.all(
|
siteDir: context.siteDir,
|
||||||
translationFiles.map((translationFile) =>
|
translationFile,
|
||||||
localizePluginTranslationFile({
|
plugin,
|
||||||
locale: context.i18n.currentLocale,
|
}),
|
||||||
siteDir: context.siteDir,
|
),
|
||||||
translationFile,
|
);
|
||||||
plugin: contentLoadedPlugin,
|
return {
|
||||||
}),
|
...plugin,
|
||||||
),
|
translationFiles: localizedTranslationFiles,
|
||||||
);
|
};
|
||||||
return {
|
}),
|
||||||
...contentLoadedPlugin,
|
);
|
||||||
translationFiles: localizedTranslationFiles,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const allContent: AllContent = _.chain(loadedPlugins)
|
const allContent: AllContent = _.chain(loadedPlugins)
|
||||||
.groupBy((item) => item.name)
|
.groupBy((item) => item.name)
|
||||||
|
@ -174,9 +173,6 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Plugin Lifecycle - routesLoaded.
|
// 4. Plugin Lifecycle - routesLoaded.
|
||||||
// Currently plugins run lifecycle methods in parallel and are not
|
|
||||||
// order-dependent. We could change this in future if there are plugins which
|
|
||||||
// need to run in certain order or depend on others for data.
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
contentLoadedTranslatedPlugins.map(async (plugin) => {
|
contentLoadedTranslatedPlugins.map(async (plugin) => {
|
||||||
if (!plugin.routesLoaded) {
|
if (!plugin.routesLoaded) {
|
||||||
|
@ -197,28 +193,24 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl);
|
sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl);
|
||||||
|
|
||||||
// Apply each plugin one after the other to translate the theme config
|
// Apply each plugin one after the other to translate the theme config
|
||||||
function translateThemeConfig(
|
const themeConfigTranslated = contentLoadedTranslatedPlugins.reduce(
|
||||||
untranslatedThemeConfig: ThemeConfig,
|
(currentThemeConfig, plugin) => {
|
||||||
): ThemeConfig {
|
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
|
||||||
return contentLoadedTranslatedPlugins.reduce(
|
themeConfig: currentThemeConfig,
|
||||||
(currentThemeConfig, plugin) => {
|
translationFiles: plugin.translationFiles,
|
||||||
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
|
});
|
||||||
themeConfig: currentThemeConfig,
|
return {
|
||||||
translationFiles: plugin.translationFiles,
|
...currentThemeConfig,
|
||||||
});
|
...translatedThemeConfigSlice,
|
||||||
return {
|
};
|
||||||
...currentThemeConfig,
|
},
|
||||||
...translatedThemeConfigSlice,
|
context.siteConfig.themeConfig,
|
||||||
};
|
);
|
||||||
},
|
|
||||||
untranslatedThemeConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: loadedPlugins,
|
plugins: loadedPlugins,
|
||||||
pluginsRouteConfigs,
|
pluginsRouteConfigs,
|
||||||
globalData,
|
globalData,
|
||||||
themeConfigTranslated: translateThemeConfig(context.siteConfig.themeConfig),
|
themeConfigTranslated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,13 @@
|
||||||
|
|
||||||
import {createRequire} from 'module';
|
import {createRequire} from 'module';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import importFresh from 'import-fresh';
|
|
||||||
import type {
|
import type {
|
||||||
PluginVersionInformation,
|
PluginVersionInformation,
|
||||||
ImportedPluginModule,
|
|
||||||
LoadContext,
|
LoadContext,
|
||||||
PluginModule,
|
PluginModule,
|
||||||
PluginConfig,
|
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
|
NormalizedPluginConfig,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import {getPluginVersion} from '../siteMetadata';
|
import {getPluginVersion} from '../siteMetadata';
|
||||||
|
@ -26,89 +24,6 @@ import {
|
||||||
} from '@docusaurus/utils-validation';
|
} from '@docusaurus/utils-validation';
|
||||||
import {loadPluginConfigs} from './configs';
|
import {loadPluginConfigs} from './configs';
|
||||||
|
|
||||||
export type NormalizedPluginConfig = {
|
|
||||||
plugin: PluginModule;
|
|
||||||
options: PluginOptions;
|
|
||||||
// Only available when a string is provided in config
|
|
||||||
pluginModule?: {
|
|
||||||
path: string;
|
|
||||||
module: ImportedPluginModule;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Different from pluginModule.path, this one is always an absolute path used
|
|
||||||
* to resolve relative paths returned from lifecycles
|
|
||||||
*/
|
|
||||||
entryPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function normalizePluginConfig(
|
|
||||||
pluginConfig: PluginConfig,
|
|
||||||
configPath: string,
|
|
||||||
): Promise<NormalizedPluginConfig> {
|
|
||||||
const pluginRequire = createRequire(configPath);
|
|
||||||
// plugins: ['./plugin']
|
|
||||||
if (typeof pluginConfig === 'string') {
|
|
||||||
const pluginModuleImport = pluginConfig;
|
|
||||||
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
|
||||||
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
|
||||||
return {
|
|
||||||
plugin: pluginModule?.default ?? pluginModule,
|
|
||||||
options: {},
|
|
||||||
pluginModule: {
|
|
||||||
path: pluginModuleImport,
|
|
||||||
module: pluginModule,
|
|
||||||
},
|
|
||||||
entryPath: pluginPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// plugins: [function plugin() { }]
|
|
||||||
if (typeof pluginConfig === 'function') {
|
|
||||||
return {
|
|
||||||
plugin: pluginConfig,
|
|
||||||
options: {},
|
|
||||||
entryPath: configPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// plugins: [
|
|
||||||
// ['./plugin',options],
|
|
||||||
// ]
|
|
||||||
if (typeof pluginConfig[0] === 'string') {
|
|
||||||
const pluginModuleImport = pluginConfig[0];
|
|
||||||
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
|
||||||
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
|
||||||
return {
|
|
||||||
plugin: pluginModule?.default ?? pluginModule,
|
|
||||||
options: pluginConfig[1],
|
|
||||||
pluginModule: {
|
|
||||||
path: pluginModuleImport,
|
|
||||||
module: pluginModule,
|
|
||||||
},
|
|
||||||
entryPath: pluginPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// plugins: [
|
|
||||||
// [function plugin() { },options],
|
|
||||||
// ]
|
|
||||||
return {
|
|
||||||
plugin: pluginConfig[0],
|
|
||||||
options: pluginConfig[1],
|
|
||||||
entryPath: configPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function normalizePluginConfigs(
|
|
||||||
pluginConfigs: PluginConfig[],
|
|
||||||
configPath: string,
|
|
||||||
): Promise<NormalizedPluginConfig[]> {
|
|
||||||
return Promise.all(
|
|
||||||
pluginConfigs.map((pluginConfig) =>
|
|
||||||
normalizePluginConfig(pluginConfig, configPath),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptionValidationFunction(
|
function getOptionValidationFunction(
|
||||||
normalizedPluginConfig: NormalizedPluginConfig,
|
normalizedPluginConfig: NormalizedPluginConfig,
|
||||||
): PluginModule['validateOptions'] {
|
): PluginModule['validateOptions'] {
|
||||||
|
@ -135,17 +50,17 @@ function getThemeValidationFunction(
|
||||||
return normalizedPluginConfig.plugin.validateThemeConfig;
|
return normalizedPluginConfig.plugin.validateThemeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the plugin constructors and returns their return values. It would load
|
||||||
|
* plugin configs from `plugins`, `themes`, and `presets`.
|
||||||
|
*/
|
||||||
export async function initPlugins(
|
export async function initPlugins(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
): Promise<InitializedPlugin[]> {
|
): Promise<InitializedPlugin[]> {
|
||||||
// We need to resolve plugins from the perspective of the siteDir, since the
|
// We need to resolve plugins from the perspective of the site config, as if
|
||||||
// siteDir's package.json declares the dependency on these plugins.
|
// we are using `require.resolve` on those module names.
|
||||||
const pluginRequire = createRequire(context.siteConfigPath);
|
const pluginRequire = createRequire(context.siteConfigPath);
|
||||||
const pluginConfigs = await loadPluginConfigs(context);
|
const pluginConfigs = await loadPluginConfigs(context);
|
||||||
const pluginConfigsNormalized = await normalizePluginConfigs(
|
|
||||||
pluginConfigs,
|
|
||||||
context.siteConfigPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function doGetPluginVersion(
|
async function doGetPluginVersion(
|
||||||
normalizedPluginConfig: NormalizedPluginConfig,
|
normalizedPluginConfig: NormalizedPluginConfig,
|
||||||
|
@ -221,7 +136,7 @@ export async function initPlugins(
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins: InitializedPlugin[] = await Promise.all(
|
const plugins: InitializedPlugin[] = await Promise.all(
|
||||||
pluginConfigsNormalized.map(initializePlugin),
|
pluginConfigs.map(initializePlugin),
|
||||||
);
|
);
|
||||||
|
|
||||||
ensureUniquePluginInstanceIds(plugins);
|
ensureUniquePluginInstanceIds(plugins);
|
||||||
|
|
|
@ -9,8 +9,10 @@ import _ from 'lodash';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import type {InitializedPlugin} from '@docusaurus/types';
|
import type {InitializedPlugin} from '@docusaurus/types';
|
||||||
|
|
||||||
// It is forbidden to have 2 plugins of the same name sharing the same id
|
/**
|
||||||
// this is required to support multi-instance plugins without conflict
|
* It is forbidden to have 2 plugins of the same name sharing the same ID.
|
||||||
|
* This is required to support multi-instance plugins without conflict.
|
||||||
|
*/
|
||||||
export function ensureUniquePluginInstanceIds(
|
export function ensureUniquePluginInstanceIds(
|
||||||
plugins: InitializedPlugin[],
|
plugins: InitializedPlugin[],
|
||||||
): void {
|
): void {
|
||||||
|
|
|
@ -11,15 +11,19 @@ import type {
|
||||||
LoadContext,
|
LoadContext,
|
||||||
PluginConfig,
|
PluginConfig,
|
||||||
ImportedPresetModule,
|
ImportedPresetModule,
|
||||||
|
DocusaurusConfig,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import {resolveModuleName} from '../moduleShorthand';
|
import {resolveModuleName} from './moduleShorthand';
|
||||||
|
|
||||||
export async function loadPresets(context: LoadContext): Promise<{
|
/**
|
||||||
plugins: PluginConfig[];
|
* Calls preset functions, aggregates each of their return values, and returns
|
||||||
themes: PluginConfig[];
|
* the plugin and theme configs.
|
||||||
}> {
|
*/
|
||||||
// We need to resolve presets from the perspective of the siteDir, since the
|
export async function loadPresets(
|
||||||
// siteDir's package.json declares the dependency on these presets.
|
context: LoadContext,
|
||||||
|
): Promise<Pick<DocusaurusConfig, 'plugins' | 'themes'>> {
|
||||||
|
// We need to resolve plugins from the perspective of the site config, as if
|
||||||
|
// we are using `require.resolve` on those module names.
|
||||||
const presetRequire = createRequire(context.siteConfigPath);
|
const presetRequire = createRequire(context.siteConfigPath);
|
||||||
|
|
||||||
const {presets} = context.siteConfig;
|
const {presets} = context.siteConfig;
|
||||||
|
|
|
@ -11,14 +11,17 @@ import {
|
||||||
removeSuffix,
|
removeSuffix,
|
||||||
simpleHash,
|
simpleHash,
|
||||||
escapePath,
|
escapePath,
|
||||||
|
reportMessage,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {stringify} from 'querystring';
|
import {stringify} from 'querystring';
|
||||||
|
import {getAllFinalRoutes} from './utils';
|
||||||
import type {
|
import type {
|
||||||
ChunkRegistry,
|
ChunkRegistry,
|
||||||
Module,
|
Module,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
RouteModule,
|
RouteModule,
|
||||||
ChunkNames,
|
ChunkNames,
|
||||||
|
ReportingSeverity,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type RegistryMap = {
|
type RegistryMap = {
|
||||||
|
@ -119,15 +122,107 @@ function getModulePath(target: Module): string {
|
||||||
return `${target.path}${queryStr}`;
|
return `${target.path}${queryStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genRouteChunkNames(
|
||||||
|
registry: RegistryMap,
|
||||||
|
value: Module,
|
||||||
|
prefix?: string,
|
||||||
|
name?: string,
|
||||||
|
): string;
|
||||||
|
function genRouteChunkNames(
|
||||||
|
registry: RegistryMap,
|
||||||
|
value: RouteModule,
|
||||||
|
prefix?: string,
|
||||||
|
name?: string,
|
||||||
|
): ChunkNames;
|
||||||
|
function genRouteChunkNames(
|
||||||
|
registry: RegistryMap,
|
||||||
|
value: RouteModule[],
|
||||||
|
prefix?: string,
|
||||||
|
name?: string,
|
||||||
|
): ChunkNames[];
|
||||||
|
function genRouteChunkNames(
|
||||||
|
registry: RegistryMap,
|
||||||
|
value: RouteModule | RouteModule[] | Module,
|
||||||
|
prefix?: string,
|
||||||
|
name?: string,
|
||||||
|
): ChunkNames | ChunkNames[] | string;
|
||||||
|
function genRouteChunkNames(
|
||||||
|
// TODO instead of passing a mutating the registry, return a registry slice?
|
||||||
|
registry: RegistryMap,
|
||||||
|
value: RouteModule | RouteModule[] | Module | null | undefined,
|
||||||
|
prefix?: string,
|
||||||
|
name?: string,
|
||||||
|
): null | string | ChunkNames | ChunkNames[] {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((val, index) =>
|
||||||
|
genRouteChunkNames(registry, val, `${index}`, name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModule(value)) {
|
||||||
|
const modulePath = getModulePath(value);
|
||||||
|
const chunkName = genChunkName(modulePath, prefix, name);
|
||||||
|
const loader = `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
||||||
|
modulePath,
|
||||||
|
)}')`;
|
||||||
|
|
||||||
|
registry[chunkName] = {loader, modulePath};
|
||||||
|
return chunkName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue: ChunkNames = {};
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
newValue[key] = genRouteChunkNames(registry, v, key, name);
|
||||||
|
});
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDuplicateRoutes(
|
||||||
|
pluginsRouteConfigs: RouteConfig[],
|
||||||
|
onDuplicateRoutes: ReportingSeverity,
|
||||||
|
): void {
|
||||||
|
if (onDuplicateRoutes === 'ignore') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map(
|
||||||
|
(routeConfig) => routeConfig.path,
|
||||||
|
);
|
||||||
|
const seenRoutes = new Set<string>();
|
||||||
|
const duplicatePaths = allRoutes.filter((route) => {
|
||||||
|
if (seenRoutes.has(route)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
seenRoutes.add(route);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (duplicatePaths.length > 0) {
|
||||||
|
const finalMessage = `Duplicate routes found!
|
||||||
|
${duplicatePaths
|
||||||
|
.map(
|
||||||
|
(duplicateRoute) =>
|
||||||
|
`- Attempting to create page at ${duplicateRoute}, but a page already exists at this route.`,
|
||||||
|
)
|
||||||
|
.join('\n')}
|
||||||
|
This could lead to non-deterministic routing behavior.`;
|
||||||
|
reportMessage(finalMessage, onDuplicateRoutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadRoutes(
|
export async function loadRoutes(
|
||||||
pluginsRouteConfigs: RouteConfig[],
|
pluginsRouteConfigs: RouteConfig[],
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
onDuplicateRoutes: ReportingSeverity,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
registry: {[chunkName: string]: ChunkRegistry};
|
registry: {[chunkName: string]: ChunkRegistry};
|
||||||
routesConfig: string;
|
routesConfig: string;
|
||||||
routesChunkNames: {[routePath: string]: ChunkNames};
|
routesChunkNames: {[routePath: string]: ChunkNames};
|
||||||
routesPaths: string[];
|
routesPaths: string[];
|
||||||
}> {
|
}> {
|
||||||
|
handleDuplicateRoutes(pluginsRouteConfigs, onDuplicateRoutes);
|
||||||
const registry: {[chunkName: string]: ChunkRegistry} = {};
|
const registry: {[chunkName: string]: ChunkRegistry} = {};
|
||||||
const routesPaths: string[] = [normalizeUrl([baseUrl, '404.html'])];
|
const routesPaths: string[] = [normalizeUrl([baseUrl, '404.html'])];
|
||||||
const routesChunkNames: {[routePath: string]: ChunkNames} = {};
|
const routesChunkNames: {[routePath: string]: ChunkNames} = {};
|
||||||
|
@ -194,63 +289,3 @@ ${indent(NotFoundRouteCode)}
|
||||||
routesPaths,
|
routesPaths,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function genRouteChunkNames(
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: Module,
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): string;
|
|
||||||
function genRouteChunkNames(
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: RouteModule,
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): ChunkNames;
|
|
||||||
function genRouteChunkNames(
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: RouteModule[],
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): ChunkNames[];
|
|
||||||
function genRouteChunkNames(
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: RouteModule | RouteModule[] | Module,
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): ChunkNames | ChunkNames[] | string;
|
|
||||||
|
|
||||||
function genRouteChunkNames(
|
|
||||||
// TODO instead of passing a mutating the registry, return a registry slice?
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: RouteModule | RouteModule[] | Module | null | undefined,
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): null | string | ChunkNames | ChunkNames[] {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map((val, index) =>
|
|
||||||
genRouteChunkNames(registry, val, `${index}`, name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isModule(value)) {
|
|
||||||
const modulePath = getModulePath(value);
|
|
||||||
const chunkName = genChunkName(modulePath, prefix, name);
|
|
||||||
const loader = `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
|
||||||
modulePath,
|
|
||||||
)}')`;
|
|
||||||
|
|
||||||
registry[chunkName] = {loader, modulePath};
|
|
||||||
return chunkName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue: ChunkNames = {};
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
newValue[key] = genRouteChunkNames(registry, v, key, name);
|
|
||||||
});
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import type {
|
import type {
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
PluginVersionInformation,
|
PluginVersionInformation,
|
||||||
DocusaurusSiteMetadata,
|
SiteMetadata,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -61,7 +61,8 @@ export async function getPluginVersion(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// In the case where a plugin is a path where no parent directory contains
|
// In the case where a plugin is a path where no parent directory contains
|
||||||
// package.json (e.g. inline plugin), we can only classify it as local.
|
// package.json, we can only classify it as local. Could happen if one puts a
|
||||||
|
// script in the parent directory of the site.
|
||||||
return {type: 'local'};
|
return {type: 'local'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ export async function getPluginVersion(
|
||||||
* @see https://github.com/facebook/docusaurus/issues/3371
|
* @see https://github.com/facebook/docusaurus/issues/3371
|
||||||
* @see https://github.com/facebook/docusaurus/pull/3386
|
* @see https://github.com/facebook/docusaurus/pull/3386
|
||||||
*/
|
*/
|
||||||
function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) {
|
function checkDocusaurusPackagesVersion(siteMetadata: SiteMetadata) {
|
||||||
const {docusaurusVersion} = siteMetadata;
|
const {docusaurusVersion} = siteMetadata;
|
||||||
Object.entries(siteMetadata.pluginVersions).forEach(
|
Object.entries(siteMetadata.pluginVersions).forEach(
|
||||||
([plugin, versionInfo]) => {
|
([plugin, versionInfo]) => {
|
||||||
|
@ -96,8 +97,8 @@ export async function loadSiteMetadata({
|
||||||
}: {
|
}: {
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
}): Promise<DocusaurusSiteMetadata> {
|
}): Promise<SiteMetadata> {
|
||||||
const siteMetadata: DocusaurusSiteMetadata = {
|
const siteMetadata: SiteMetadata = {
|
||||||
docusaurusVersion: (await getPackageJsonVersion(
|
docusaurusVersion: (await getPackageJsonVersion(
|
||||||
path.join(__dirname, '../../package.json'),
|
path.join(__dirname, '../../package.json'),
|
||||||
))!,
|
))!,
|
||||||
|
|
|
@ -1,61 +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 path from 'path';
|
|
||||||
import {loadThemeAliases} from '../index';
|
|
||||||
|
|
||||||
describe('loadThemeAliases', () => {
|
|
||||||
it('next alias can override the previous alias', async () => {
|
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
|
||||||
const theme1Path = path.join(fixtures, 'theme-1');
|
|
||||||
const theme2Path = path.join(fixtures, 'theme-2');
|
|
||||||
|
|
||||||
const alias = await loadThemeAliases([theme1Path, theme2Path], []);
|
|
||||||
|
|
||||||
// Testing entries, because order matters!
|
|
||||||
expect(Object.entries(alias)).toEqual(
|
|
||||||
Object.entries({
|
|
||||||
'@theme-init/Layout': path.join(theme1Path, 'Layout.js'),
|
|
||||||
|
|
||||||
'@theme-original/Footer': path.join(theme1Path, 'Footer/index.js'),
|
|
||||||
'@theme-original/Layout': path.join(theme2Path, 'Layout/index.js'),
|
|
||||||
'@theme-original/Navbar': path.join(theme2Path, 'Navbar.js'),
|
|
||||||
'@theme-original/NavbarItem/NestedNavbarItem': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/NestedNavbarItem/index.js',
|
|
||||||
),
|
|
||||||
'@theme-original/NavbarItem/SiblingNavbarItem': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/SiblingNavbarItem.js',
|
|
||||||
),
|
|
||||||
'@theme-original/NavbarItem/zzz': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/zzz.js',
|
|
||||||
),
|
|
||||||
'@theme-original/NavbarItem': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/index.js',
|
|
||||||
),
|
|
||||||
|
|
||||||
'@theme/Footer': path.join(theme1Path, 'Footer/index.js'),
|
|
||||||
'@theme/Layout': path.join(theme2Path, 'Layout/index.js'),
|
|
||||||
'@theme/Navbar': path.join(theme2Path, 'Navbar.js'),
|
|
||||||
'@theme/NavbarItem/NestedNavbarItem': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/NestedNavbarItem/index.js',
|
|
||||||
),
|
|
||||||
'@theme/NavbarItem/SiblingNavbarItem': path.join(
|
|
||||||
theme2Path,
|
|
||||||
'NavbarItem/SiblingNavbarItem.js',
|
|
||||||
),
|
|
||||||
'@theme/NavbarItem/zzz': path.join(theme2Path, 'NavbarItem/zzz.js'),
|
|
||||||
'@theme/NavbarItem': path.join(theme2Path, 'NavbarItem/index.js'),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(alias).not.toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,62 +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 fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
|
||||||
import {fileToPath, posixPath, normalizeUrl, Globby} from '@docusaurus/utils';
|
|
||||||
import type {ThemeAliases} from '@docusaurus/types';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
// Order of Webpack aliases is important because one alias can shadow another
|
|
||||||
// This ensure @theme/NavbarItem alias is after @theme/NavbarItem/LocaleDropdown
|
|
||||||
// See https://github.com/facebook/docusaurus/pull/3922
|
|
||||||
// See https://github.com/facebook/docusaurus/issues/5382
|
|
||||||
export function sortAliases(aliases: ThemeAliases): ThemeAliases {
|
|
||||||
// Alphabetical order by default
|
|
||||||
const entries = _.sortBy(Object.entries(aliases), ([alias]) => alias);
|
|
||||||
// @theme/NavbarItem should be after @theme/NavbarItem/LocaleDropdown
|
|
||||||
entries.sort(([alias1], [alias2]) =>
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
alias1.includes(`${alias2}/`) ? -1 : alias2.includes(`${alias1}/`) ? 1 : 0,
|
|
||||||
);
|
|
||||||
return Object.fromEntries(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function themeAlias(
|
|
||||||
themePath: string,
|
|
||||||
addOriginalAlias: boolean,
|
|
||||||
): Promise<ThemeAliases> {
|
|
||||||
if (!(await fs.pathExists(themePath))) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeComponentFiles = await Globby(['**/*.{js,jsx,ts,tsx}'], {
|
|
||||||
cwd: themePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const aliases: ThemeAliases = {};
|
|
||||||
|
|
||||||
themeComponentFiles.forEach((relativeSource) => {
|
|
||||||
const filePath = path.join(themePath, relativeSource);
|
|
||||||
const fileName = fileToPath(relativeSource);
|
|
||||||
|
|
||||||
const aliasName = posixPath(
|
|
||||||
normalizeUrl(['@theme', fileName]).replace(/\/$/, ''),
|
|
||||||
);
|
|
||||||
aliases[aliasName] = filePath;
|
|
||||||
|
|
||||||
if (addOriginalAlias) {
|
|
||||||
// For swizzled components to access the original.
|
|
||||||
const originalAliasName = posixPath(
|
|
||||||
normalizeUrl(['@theme-original', fileName]).replace(/\/$/, ''),
|
|
||||||
);
|
|
||||||
aliases[originalAliasName] = filePath;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortAliases(aliases);
|
|
||||||
}
|
|
|
@ -1,61 +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 path from 'path';
|
|
||||||
import {THEME_PATH} from '@docusaurus/utils';
|
|
||||||
import {themeAlias, sortAliases} from './alias';
|
|
||||||
import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types';
|
|
||||||
|
|
||||||
const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback');
|
|
||||||
|
|
||||||
export async function loadThemeAliases(
|
|
||||||
themePaths: string[],
|
|
||||||
userThemePaths: string[],
|
|
||||||
): Promise<ThemeAliases> {
|
|
||||||
const aliases: ThemeAliases = {};
|
|
||||||
|
|
||||||
for (const themePath of themePaths) {
|
|
||||||
const themeAliases = await themeAlias(themePath, true);
|
|
||||||
Object.entries(themeAliases).forEach(([aliasKey, alias]) => {
|
|
||||||
// If this alias shadows a previous one, use @theme-init to preserve the
|
|
||||||
// initial one. @theme-init is only applied once: to the initial theme
|
|
||||||
// that provided this component
|
|
||||||
if (aliasKey in aliases) {
|
|
||||||
const componentName = aliasKey.substring(aliasKey.indexOf('/') + 1);
|
|
||||||
const initAlias = `@theme-init/${componentName}`;
|
|
||||||
if (!(initAlias in aliases)) {
|
|
||||||
aliases[initAlias] = aliases[aliasKey]!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
aliases[aliasKey] = alias;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const themePath of userThemePaths) {
|
|
||||||
const userThemeAliases = await themeAlias(themePath, false);
|
|
||||||
Object.assign(aliases, userThemeAliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortAliases(aliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadPluginsThemeAliases({
|
|
||||||
siteDir,
|
|
||||||
plugins,
|
|
||||||
}: {
|
|
||||||
siteDir: string;
|
|
||||||
plugins: LoadedPlugin[];
|
|
||||||
}): Promise<ThemeAliases> {
|
|
||||||
const pluginThemes: string[] = plugins
|
|
||||||
.map(
|
|
||||||
(plugin) =>
|
|
||||||
plugin.getThemePath && path.resolve(plugin.path, plugin.getThemePath()),
|
|
||||||
)
|
|
||||||
.filter((x): x is string => Boolean(x));
|
|
||||||
const userTheme = path.resolve(siteDir, THEME_PATH);
|
|
||||||
return loadThemeAliases([ThemeFallbackDir, ...pluginThemes], [userTheme]);
|
|
||||||
}
|
|
|
@ -144,13 +144,10 @@ Maybe you should remove them? ${unknownKeys}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// should we make this configurable?
|
// should we make this configurable?
|
||||||
function getTranslationsDirPath(context: TranslationContext): string {
|
|
||||||
return path.join(context.siteDir, I18N_DIR_NAME);
|
|
||||||
}
|
|
||||||
export function getTranslationsLocaleDirPath(
|
export function getTranslationsLocaleDirPath(
|
||||||
context: TranslationContext,
|
context: TranslationContext,
|
||||||
): string {
|
): string {
|
||||||
return path.join(getTranslationsDirPath(context), context.locale);
|
return path.join(context.siteDir, I18N_DIR_NAME, context.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodeTranslationsFilePath(context: TranslationContext): string {
|
function getCodeTranslationsFilePath(context: TranslationContext): string {
|
||||||
|
|
|
@ -47,26 +47,3 @@ exports[`base webpack config creates webpack aliases 1`] = `
|
||||||
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",
|
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDocusaurusAliases() returns appropriate webpack aliases 1`] = `
|
|
||||||
{
|
|
||||||
"@docusaurus/BrowserOnly": "../../client/exports/BrowserOnly.tsx",
|
|
||||||
"@docusaurus/ComponentCreator": "../../client/exports/ComponentCreator.tsx",
|
|
||||||
"@docusaurus/ErrorBoundary": "../../client/exports/ErrorBoundary.tsx",
|
|
||||||
"@docusaurus/ExecutionEnvironment": "../../client/exports/ExecutionEnvironment.ts",
|
|
||||||
"@docusaurus/Head": "../../client/exports/Head.tsx",
|
|
||||||
"@docusaurus/Interpolate": "../../client/exports/Interpolate.tsx",
|
|
||||||
"@docusaurus/Link": "../../client/exports/Link.tsx",
|
|
||||||
"@docusaurus/Noop": "../../client/exports/Noop.ts",
|
|
||||||
"@docusaurus/Translate": "../../client/exports/Translate.tsx",
|
|
||||||
"@docusaurus/constants": "../../client/exports/constants.ts",
|
|
||||||
"@docusaurus/isInternalUrl": "../../client/exports/isInternalUrl.ts",
|
|
||||||
"@docusaurus/renderRoutes": "../../client/exports/renderRoutes.ts",
|
|
||||||
"@docusaurus/router": "../../client/exports/router.ts",
|
|
||||||
"@docusaurus/useBaseUrl": "../../client/exports/useBaseUrl.ts",
|
|
||||||
"@docusaurus/useDocusaurusContext": "../../client/exports/useDocusaurusContext.ts",
|
|
||||||
"@docusaurus/useGlobalData": "../../client/exports/useGlobalData.ts",
|
|
||||||
"@docusaurus/useIsBrowser": "../../client/exports/useIsBrowser.ts",
|
|
||||||
"@docusaurus/useRouteContext": "../../client/exports/useRouteContext.tsx",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
|
@ -8,12 +8,7 @@
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {excludeJS, clientDir, createBaseConfig} from '../base';
|
||||||
excludeJS,
|
|
||||||
clientDir,
|
|
||||||
getDocusaurusAliases,
|
|
||||||
createBaseConfig,
|
|
||||||
} from '../base';
|
|
||||||
import * as utils from '@docusaurus/utils/lib/webpackUtils';
|
import * as utils from '@docusaurus/utils/lib/webpackUtils';
|
||||||
import {posixPath} from '@docusaurus/utils';
|
import {posixPath} from '@docusaurus/utils';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
@ -68,17 +63,6 @@ describe('babel transpilation exclude logic', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getDocusaurusAliases()', () => {
|
|
||||||
it('returns appropriate webpack aliases', async () => {
|
|
||||||
// using relative paths makes tests work everywhere
|
|
||||||
const relativeDocusaurusAliases = _.mapValues(
|
|
||||||
await getDocusaurusAliases(),
|
|
||||||
(aliasValue) => posixPath(path.relative(__dirname, aliasValue)),
|
|
||||||
);
|
|
||||||
expect(relativeDocusaurusAliases).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('base webpack config', () => {
|
describe('base webpack config', () => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
outDir: '',
|
outDir: '',
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = `
|
||||||
|
{
|
||||||
|
"@docusaurus/BrowserOnly": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/BrowserOnly.tsx",
|
||||||
|
"@docusaurus/ComponentCreator": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ComponentCreator.tsx",
|
||||||
|
"@docusaurus/ErrorBoundary": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ErrorBoundary.tsx",
|
||||||
|
"@docusaurus/ExecutionEnvironment": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ExecutionEnvironment.ts",
|
||||||
|
"@docusaurus/Head": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Head.tsx",
|
||||||
|
"@docusaurus/Interpolate": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Interpolate.tsx",
|
||||||
|
"@docusaurus/Link": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Link.tsx",
|
||||||
|
"@docusaurus/Noop": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Noop.ts",
|
||||||
|
"@docusaurus/Translate": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Translate.tsx",
|
||||||
|
"@docusaurus/constants": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/constants.ts",
|
||||||
|
"@docusaurus/isInternalUrl": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/isInternalUrl.ts",
|
||||||
|
"@docusaurus/renderRoutes": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/renderRoutes.ts",
|
||||||
|
"@docusaurus/router": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/router.ts",
|
||||||
|
"@docusaurus/useBaseUrl": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useBaseUrl.ts",
|
||||||
|
"@docusaurus/useDocusaurusContext": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useDocusaurusContext.ts",
|
||||||
|
"@docusaurus/useGlobalData": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useGlobalData.ts",
|
||||||
|
"@docusaurus/useIsBrowser": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useIsBrowser.ts",
|
||||||
|
"@docusaurus/useRouteContext": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useRouteContext.tsx",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`loadThemeAliases next alias can override the previous alias 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"@theme-init/Layout",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Layout/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Error",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Footer",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Layout",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Loading",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Navbar",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/NavbarItem/NestedNavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/NavbarItem/SiblingNavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/NavbarItem/zzz",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/NavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/NotFound",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/Root",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Root/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/SiteMetadata",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Error",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Footer",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Layout",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Loading",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Navbar",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/NavbarItem/NestedNavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/NavbarItem/SiblingNavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/NavbarItem/zzz",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/NavbarItem",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/NotFound",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/Root",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Root/index.tsx",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@theme/SiteMetadata",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
|
@ -5,9 +5,14 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import {themeAlias, sortAliases} from '../alias';
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
loadThemeAliases,
|
||||||
|
loadDocusaurusAliases,
|
||||||
|
sortAliases,
|
||||||
|
createAliasesForTheme,
|
||||||
|
} from '../index';
|
||||||
|
|
||||||
describe('sortAliases', () => {
|
describe('sortAliases', () => {
|
||||||
// https://github.com/facebook/docusaurus/issues/6878
|
// https://github.com/facebook/docusaurus/issues/6878
|
||||||
|
@ -53,11 +58,11 @@ describe('sortAliases', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('themeAlias', () => {
|
describe('createAliasesForTheme', () => {
|
||||||
it('valid themePath 1 with components', async () => {
|
it('creates aliases for themePath 1 with components', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'theme-1');
|
const themePath = path.join(fixtures, 'theme-1');
|
||||||
const alias = await themeAlias(themePath, true);
|
const alias = await createAliasesForTheme(themePath, true);
|
||||||
// Testing entries, because order matters!
|
// Testing entries, because order matters!
|
||||||
expect(Object.entries(alias)).toEqual(
|
expect(Object.entries(alias)).toEqual(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
@ -70,10 +75,10 @@ describe('themeAlias', () => {
|
||||||
expect(alias).not.toEqual({});
|
expect(alias).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('valid themePath 1 with components without original', async () => {
|
it('creates aliases for themePath 1 with components without original', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'theme-1');
|
const themePath = path.join(fixtures, 'theme-1');
|
||||||
const alias = await themeAlias(themePath, false);
|
const alias = await createAliasesForTheme(themePath, false);
|
||||||
// Testing entries, because order matters!
|
// Testing entries, because order matters!
|
||||||
expect(Object.entries(alias)).toEqual(
|
expect(Object.entries(alias)).toEqual(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
@ -84,10 +89,10 @@ describe('themeAlias', () => {
|
||||||
expect(alias).not.toEqual({});
|
expect(alias).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('valid themePath 2 with components', async () => {
|
it('creates aliases for themePath 2 with components', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'theme-2');
|
const themePath = path.join(fixtures, 'theme-2');
|
||||||
const alias = await themeAlias(themePath, true);
|
const alias = await createAliasesForTheme(themePath, true);
|
||||||
// Testing entries, because order matters!
|
// Testing entries, because order matters!
|
||||||
expect(Object.entries(alias)).toEqual(
|
expect(Object.entries(alias)).toEqual(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
@ -127,10 +132,10 @@ describe('themeAlias', () => {
|
||||||
expect(alias).not.toEqual({});
|
expect(alias).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('valid themePath 2 with components without original', async () => {
|
it('creates aliases for themePath 2 with components without original', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'theme-2');
|
const themePath = path.join(fixtures, 'theme-2');
|
||||||
const alias = await themeAlias(themePath, false);
|
const alias = await createAliasesForTheme(themePath, false);
|
||||||
// Testing entries, because order matters!
|
// Testing entries, because order matters!
|
||||||
expect(Object.entries(alias)).toEqual(
|
expect(Object.entries(alias)).toEqual(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
@ -151,26 +156,51 @@ describe('themeAlias', () => {
|
||||||
expect(alias).not.toEqual({});
|
expect(alias).not.toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('valid themePath with no components', async () => {
|
it('creates themePath with no components', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'empty-theme');
|
const themePath = path.join(fixtures, 'empty-theme');
|
||||||
await fs.ensureDir(themePath);
|
await fs.ensureDir(themePath);
|
||||||
const alias = await themeAlias(themePath, true);
|
const alias = await createAliasesForTheme(themePath, true);
|
||||||
expect(alias).toEqual({});
|
expect(alias).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('valid themePath with no components without original', async () => {
|
it('creates themePath with no components without original', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, 'empty-theme');
|
const themePath = path.join(fixtures, 'empty-theme');
|
||||||
await fs.ensureDir(themePath);
|
await fs.ensureDir(themePath);
|
||||||
const alias = await themeAlias(themePath, false);
|
const alias = await createAliasesForTheme(themePath, false);
|
||||||
expect(alias).toEqual({});
|
expect(alias).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invalid themePath that does not exist', async () => {
|
it('creates nothing for invalid themePath that does not exist', async () => {
|
||||||
const fixtures = path.join(__dirname, '__fixtures__');
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
const themePath = path.join(fixtures, '__noExist__');
|
const themePath = path.join(fixtures, '__noExist__');
|
||||||
const alias = await themeAlias(themePath, true);
|
const alias = await createAliasesForTheme(themePath, true);
|
||||||
expect(alias).toEqual({});
|
expect(alias).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDocusaurusAliases', () => {
|
||||||
|
it('returns appropriate webpack aliases', async () => {
|
||||||
|
await expect(loadDocusaurusAliases()).resolves.toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadThemeAliases', () => {
|
||||||
|
it('next alias can override the previous alias', async () => {
|
||||||
|
const fixtures = path.join(__dirname, '__fixtures__');
|
||||||
|
const theme1Path = path.join(fixtures, 'theme-1');
|
||||||
|
const theme2Path = path.join(fixtures, 'theme-2');
|
||||||
|
|
||||||
|
const alias = await loadThemeAliases({
|
||||||
|
siteDir: fixtures,
|
||||||
|
plugins: [
|
||||||
|
{getThemePath: () => theme1Path},
|
||||||
|
{getThemePath: () => theme2Path},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Testing entries, because order matters!
|
||||||
|
expect(Object.entries(alias)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
149
packages/docusaurus/src/webpack/aliases/index.ts
Normal file
149
packages/docusaurus/src/webpack/aliases/index.ts
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* 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 fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
THEME_PATH,
|
||||||
|
fileToPath,
|
||||||
|
posixPath,
|
||||||
|
normalizeUrl,
|
||||||
|
Globby,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types';
|
||||||
|
|
||||||
|
const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order of Webpack aliases is important because one alias can shadow another.
|
||||||
|
* This ensures `@theme/NavbarItem` alias is after
|
||||||
|
* `@theme/NavbarItem/LocaleDropdown`.
|
||||||
|
*
|
||||||
|
* @see https://github.com/facebook/docusaurus/pull/3922
|
||||||
|
* @see https://github.com/facebook/docusaurus/issues/5382
|
||||||
|
*/
|
||||||
|
export function sortAliases(aliases: ThemeAliases): ThemeAliases {
|
||||||
|
// Alphabetical order by default
|
||||||
|
const entries = _.sortBy(Object.entries(aliases), ([alias]) => alias);
|
||||||
|
// @theme/NavbarItem should be after @theme/NavbarItem/LocaleDropdown
|
||||||
|
entries.sort(([alias1], [alias2]) =>
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
alias1.includes(`${alias2}/`) ? -1 : alias2.includes(`${alias1}/`) ? 1 : 0,
|
||||||
|
);
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAliasesForTheme(
|
||||||
|
themePath: string,
|
||||||
|
addOriginalAlias: boolean,
|
||||||
|
): Promise<ThemeAliases> {
|
||||||
|
if (!(await fs.pathExists(themePath))) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeComponentFiles = await Globby(['**/*.{js,jsx,ts,tsx}'], {
|
||||||
|
cwd: themePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliases: ThemeAliases = {};
|
||||||
|
|
||||||
|
themeComponentFiles.forEach((relativeSource) => {
|
||||||
|
const filePath = path.join(themePath, relativeSource);
|
||||||
|
const fileName = fileToPath(relativeSource);
|
||||||
|
|
||||||
|
const aliasName = posixPath(
|
||||||
|
normalizeUrl(['@theme', fileName]).replace(/\/$/, ''),
|
||||||
|
);
|
||||||
|
aliases[aliasName] = filePath;
|
||||||
|
|
||||||
|
if (addOriginalAlias) {
|
||||||
|
// For swizzled components to access the original.
|
||||||
|
const originalAliasName = posixPath(
|
||||||
|
normalizeUrl(['@theme-original', fileName]).replace(/\/$/, ''),
|
||||||
|
);
|
||||||
|
aliases[originalAliasName] = filePath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortAliases(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createThemeAliases(
|
||||||
|
themePaths: string[],
|
||||||
|
userThemePaths: string[],
|
||||||
|
): Promise<ThemeAliases> {
|
||||||
|
const aliases: ThemeAliases = {};
|
||||||
|
|
||||||
|
for (const themePath of themePaths) {
|
||||||
|
const themeAliases = await createAliasesForTheme(themePath, true);
|
||||||
|
Object.entries(themeAliases).forEach(([aliasKey, alias]) => {
|
||||||
|
// If this alias shadows a previous one, use @theme-init to preserve the
|
||||||
|
// initial one. @theme-init is only applied once: to the initial theme
|
||||||
|
// that provided this component
|
||||||
|
if (aliasKey in aliases) {
|
||||||
|
const componentName = aliasKey.substring(aliasKey.indexOf('/') + 1);
|
||||||
|
const initAlias = `@theme-init/${componentName}`;
|
||||||
|
if (!(initAlias in aliases)) {
|
||||||
|
aliases[initAlias] = aliases[aliasKey]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliases[aliasKey] = alias;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const themePath of userThemePaths) {
|
||||||
|
const userThemeAliases = await createAliasesForTheme(themePath, false);
|
||||||
|
Object.assign(aliases, userThemeAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortAliases(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadThemeAliases({
|
||||||
|
siteDir,
|
||||||
|
plugins,
|
||||||
|
}: {
|
||||||
|
siteDir: string;
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
}): Promise<ThemeAliases> {
|
||||||
|
const pluginThemes: string[] = plugins
|
||||||
|
.map(
|
||||||
|
(plugin) =>
|
||||||
|
plugin.getThemePath && path.resolve(plugin.path, plugin.getThemePath()),
|
||||||
|
)
|
||||||
|
.filter((x): x is string => Boolean(x));
|
||||||
|
const userTheme = path.resolve(siteDir, THEME_PATH);
|
||||||
|
return createThemeAliases([ThemeFallbackDir, ...pluginThemes], [userTheme]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: a `@docusaurus` alias would also catch `@docusaurus/theme-common`, so
|
||||||
|
* instead of naively aliasing this to `client/exports`, we use fine-grained
|
||||||
|
* aliases instead.
|
||||||
|
*/
|
||||||
|
export async function loadDocusaurusAliases(): Promise<{
|
||||||
|
[aliasName: string]: string;
|
||||||
|
}> {
|
||||||
|
const dirPath = path.resolve(__dirname, '../../client/exports');
|
||||||
|
const extensions = ['.js', '.ts', '.tsx'];
|
||||||
|
|
||||||
|
const aliases: {[key: string]: string} = {};
|
||||||
|
|
||||||
|
(await fs.readdir(dirPath))
|
||||||
|
.filter((fileName) => extensions.includes(path.extname(fileName)))
|
||||||
|
.forEach((fileName) => {
|
||||||
|
const fileNameWithoutExtension = path.basename(
|
||||||
|
fileName,
|
||||||
|
path.extname(fileName),
|
||||||
|
);
|
||||||
|
const aliasName = `@docusaurus/${fileNameWithoutExtension}`;
|
||||||
|
aliases[aliasName] = path.resolve(dirPath, fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return aliases;
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import {
|
||||||
getCustomBabelConfigFilePath,
|
getCustomBabelConfigFilePath,
|
||||||
getMinimizer,
|
getMinimizer,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import {loadPluginsThemeAliases} from '../server/themes';
|
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
|
||||||
import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils';
|
import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils';
|
||||||
|
|
||||||
const CSS_REGEX = /\.css$/i;
|
const CSS_REGEX = /\.css$/i;
|
||||||
|
@ -44,28 +44,6 @@ export function excludeJS(modulePath: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDocusaurusAliases(): Promise<{
|
|
||||||
[aliasName: string]: string;
|
|
||||||
}> {
|
|
||||||
const dirPath = path.resolve(__dirname, '../client/exports');
|
|
||||||
const extensions = ['.js', '.ts', '.tsx'];
|
|
||||||
|
|
||||||
const aliases: {[key: string]: string} = {};
|
|
||||||
|
|
||||||
(await fs.readdir(dirPath))
|
|
||||||
.filter((fileName) => extensions.includes(path.extname(fileName)))
|
|
||||||
.forEach((fileName) => {
|
|
||||||
const fileNameWithoutExtension = path.basename(
|
|
||||||
fileName,
|
|
||||||
path.extname(fileName),
|
|
||||||
);
|
|
||||||
const aliasName = `@docusaurus/${fileNameWithoutExtension}`;
|
|
||||||
aliases[aliasName] = path.resolve(dirPath, fileName);
|
|
||||||
});
|
|
||||||
|
|
||||||
return aliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createBaseConfig(
|
export async function createBaseConfig(
|
||||||
props: Props,
|
props: Props,
|
||||||
isServer: boolean,
|
isServer: boolean,
|
||||||
|
@ -92,7 +70,7 @@ export async function createBaseConfig(
|
||||||
const name = isServer ? 'server' : 'client';
|
const name = isServer ? 'server' : 'client';
|
||||||
const mode = isProd ? 'production' : 'development';
|
const mode = isProd ? 'production' : 'development';
|
||||||
|
|
||||||
const themeAliases = await loadPluginsThemeAliases({siteDir, plugins});
|
const themeAliases = await loadThemeAliases({siteDir, plugins});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
|
@ -156,11 +134,7 @@ export async function createBaseConfig(
|
||||||
alias: {
|
alias: {
|
||||||
'@site': siteDir,
|
'@site': siteDir,
|
||||||
'@generated': generatedFilesDir,
|
'@generated': generatedFilesDir,
|
||||||
|
...(await loadDocusaurusAliases()),
|
||||||
// Note: a @docusaurus alias would also catch @docusaurus/theme-common,
|
|
||||||
// so we use fine-grained aliases instead
|
|
||||||
// '@docusaurus': path.resolve(__dirname, '../client/exports'),
|
|
||||||
...(await getDocusaurusAliases()),
|
|
||||||
...themeAliases,
|
...themeAliases,
|
||||||
},
|
},
|
||||||
// This allows you to set a fallback for where Webpack should look for
|
// This allows you to set a fallback for where Webpack should look for
|
||||||
|
|
|
@ -341,7 +341,7 @@ type PluginVersionInformation =
|
||||||
| {readonly type: 'local'}
|
| {readonly type: 'local'}
|
||||||
| {readonly type: 'synthetic'};
|
| {readonly type: 'synthetic'};
|
||||||
|
|
||||||
interface DocusaurusSiteMetadata {
|
interface SiteMetadata {
|
||||||
readonly docusaurusVersion: string;
|
readonly docusaurusVersion: string;
|
||||||
readonly siteVersion?: string;
|
readonly siteVersion?: string;
|
||||||
readonly pluginVersions: Record<string, PluginVersionInformation>;
|
readonly pluginVersions: Record<string, PluginVersionInformation>;
|
||||||
|
@ -361,7 +361,7 @@ interface I18n {
|
||||||
|
|
||||||
interface DocusaurusContext {
|
interface DocusaurusContext {
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
siteMetadata: DocusaurusSiteMetadata;
|
siteMetadata: SiteMetadata;
|
||||||
globalData: Record<string, unknown>;
|
globalData: Record<string, unknown>;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
codeTranslations: Record<string, string>;
|
codeTranslations: Record<string, string>;
|
||||||
|
|
Loading…
Add table
Reference in a new issue