refactor(core): reorganize files (#7042)

* refactor(core): reorganize files

* fix types
This commit is contained in:
Joshua Chen 2022-03-28 21:49:37 +08:00 committed by GitHub
parent 85a79fd9b9
commit 5fb09a2946
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1089 additions and 1028 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?.() ?? []);
} }

View file

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

View file

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

View file

@ -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."
`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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