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' {
import type {DocusaurusSiteMetadata} from '@docusaurus/types';
import type {SiteMetadata} from '@docusaurus/types';
const siteMetadata: DocusaurusSiteMetadata;
const siteMetadata: SiteMetadata;
export = siteMetadata;
}

View file

@ -6,7 +6,7 @@
*/
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';
function translateListPage(
@ -27,7 +27,7 @@ function translateListPage(
});
}
export function getTranslationFiles(options: PluginOptions): TranslationFiles {
export function getTranslationFiles(options: PluginOptions): TranslationFile[] {
return [
{
path: 'options',
@ -51,7 +51,7 @@ export function getTranslationFiles(options: PluginOptions): TranslationFiles {
export function translateContent(
content: BlogContent,
translationFiles: TranslationFiles,
translationFiles: TranslationFile[],
): BlogContent {
const {content: optionsTranslations} = translationFiles[0]!;
return {

View file

@ -168,7 +168,7 @@ describe('simple site', () => {
loadSiteOptions: {options: Partial<PluginOptions>} = {options: {}},
) {
const siteDir = path.join(fixtureDir, 'simple-site');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const options = {
id: DEFAULT_PLUGIN_ID,
...DEFAULT_OPTIONS,
@ -523,7 +523,8 @@ describe('versioned site', () => {
},
) {
const siteDir = path.join(fixtureDir, 'versioned-site');
const context = await loadContext(siteDir, {
const context = await loadContext({
siteDir,
locale: loadSiteOptions.locale,
});
const options = {

View file

@ -115,7 +115,7 @@ Entries created:
describe('sidebar', () => {
it('site with wrong sidebar content', async () => {
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 plugin = await pluginContentDocs(
context,
@ -131,7 +131,7 @@ describe('sidebar', () => {
it('site with wrong sidebar file path', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
await expect(async () => {
const plugin = await pluginContentDocs(
@ -155,7 +155,7 @@ describe('sidebar', () => {
it('site with undefined sidebar', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({
@ -173,7 +173,7 @@ describe('sidebar', () => {
it('site with disabled sidebar', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({
@ -194,7 +194,7 @@ describe('empty/no docs website', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');
it('no files in docs folder', async () => {
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = await pluginContentDocs(
context,
@ -208,7 +208,7 @@ describe('empty/no docs website', () => {
});
it('docs folder does not exist', async () => {
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
await expect(
pluginContentDocs(
context,
@ -228,7 +228,7 @@ describe('empty/no docs website', () => {
describe('simple website', () => {
async function loadSite() {
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 plugin = await pluginContentDocs(
context,
@ -341,7 +341,7 @@ describe('simple website', () => {
describe('versioned website', () => {
async function loadSite() {
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 routeBasePath = 'docs';
const plugin = await pluginContentDocs(
@ -470,7 +470,7 @@ describe('versioned website', () => {
describe('versioned website (community)', () => {
async function loadSite() {
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 routeBasePath = 'community';
const pluginId = 'community';
@ -578,7 +578,7 @@ describe('versioned website (community)', () => {
describe('site with doc label', () => {
async function loadSite() {
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 plugin = await pluginContentDocs(
context,
@ -620,7 +620,7 @@ describe('site with full autogenerated sidebar', () => {
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({
@ -675,7 +675,7 @@ describe('site with partial autogenerated sidebars', () => {
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir, {});
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({
@ -731,7 +731,7 @@ describe('site with partial autogenerated sidebars 2 (fix #4638)', () => {
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir, {});
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({
@ -768,7 +768,7 @@ describe('site with custom sidebar items generator', () => {
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentDocs(
context,
validateOptions({

View file

@ -22,7 +22,6 @@ import {
import type {
TranslationFileContent,
TranslationFile,
TranslationFiles,
TranslationMessage,
} from '@docusaurus/types';
import {mergeTranslations} from '@docusaurus/utils';
@ -242,7 +241,7 @@ function translateSidebars(
);
}
function getVersionTranslationFiles(version: LoadedVersion): TranslationFiles {
function getVersionTranslationFiles(version: LoadedVersion): TranslationFile[] {
const versionTranslations: TranslationFileContent = {
'version.label': {
message: version.label,
@ -283,7 +282,7 @@ function translateVersion(
function getVersionsTranslationFiles(
versions: LoadedVersion[],
): TranslationFiles {
): TranslationFile[] {
return versions.flatMap(getVersionTranslationFiles);
}
function translateVersions(
@ -295,7 +294,7 @@ function translateVersions(
export function getLoadedContentTranslationFiles(
loadedContent: LoadedContent,
): TranslationFiles {
): TranslationFile[] {
return getVersionsTranslationFiles(loadedContent.loadedVersions);
}
export function translateLoadedContent(

View file

@ -15,7 +15,7 @@ import {normalizePluginOptions} from '@docusaurus/utils-validation';
describe('docusaurus-plugin-content-pages', () => {
it('loads simple pages', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentPages(
context,
validateOptions({
@ -32,7 +32,7 @@ describe('docusaurus-plugin-content-pages', () => {
it('loads simple pages with french translations', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugin = await pluginContentPages(
{
...context,

View file

@ -10,7 +10,11 @@ import type {CustomizeRuleString} from 'webpack-merge/dist/types';
import type {CommanderStatic} from 'commander';
import type {ParsedUrlQueryInput} from 'querystring';
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 Loadable from 'react-loadable';
@ -20,16 +24,23 @@ export type ThemeConfig = {
[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;
baseUrlIssueBanner: boolean;
favicon?: string;
tagline: string;
title: 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;
i18n: I18nConfig;
onBrokenLinks: ReportingSeverity;
@ -69,19 +80,16 @@ export interface DocusaurusConfig {
webpack?: {
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
// file. See https://docusaurus.io/docs/typescript-support
export type Config = Overwrite<
Partial<DocusaurusConfig>,
{
title: Required<DocusaurusConfig['title']>;
url: Required<DocusaurusConfig['url']>;
baseUrl: Required<DocusaurusConfig['baseUrl']>;
i18n?: DeepPartial<DocusaurusConfig['i18n']>;
}
/**
* Docusaurus config, as provided by the user (partial/unnormalized). This type
* is used to provide type-safety / IDE auto-complete on the config file.
* @see https://docusaurus.io/docs/typescript-support
*/
export type Config = RequireKeys<
DeepPartial<DocusaurusConfig>,
'title' | 'url' | 'baseUrl'
>;
/**
@ -101,11 +109,11 @@ export type PluginVersionInformation =
| {readonly type: 'local'}
| {readonly type: 'synthetic'};
export interface DocusaurusSiteMetadata {
export type SiteMetadata = {
readonly docusaurusVersion: string;
readonly siteVersion?: string;
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
}
};
// Inspired by Chrome JSON, because it's a widely supported i18n format
// https://developer.chrome.com/apps/i18n-messages
@ -116,7 +124,6 @@ export interface DocusaurusSiteMetadata {
export type TranslationMessage = {message: string; description?: string};
export type TranslationFileContent = {[key: string]: TranslationMessage};
export type TranslationFile = {path: string; content: TranslationFileContent};
export type TranslationFiles = TranslationFile[];
export type I18nLocaleConfig = {
label: string;
@ -134,9 +141,9 @@ export type I18n = DeepRequired<I18nConfig> & {currentLocale: string};
export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
export interface DocusaurusContext {
export type DocusaurusContext = {
siteConfig: DocusaurusConfig;
siteMetadata: DocusaurusSiteMetadata;
siteMetadata: SiteMetadata;
globalData: GlobalData;
i18n: I18n;
codeTranslations: {[msgId: string]: string};
@ -144,12 +151,12 @@ export interface DocusaurusContext {
// Don't put mutable values here, to avoid triggering re-renders
// We could reconsider that choice if context selectors are implemented
// isBrowser: boolean; // Not here on purpose!
}
};
export interface Preset {
export type Preset = {
plugins?: PluginConfig[];
themes?: PluginConfig[];
}
};
export type PresetModule = {
<T>(context: LoadContext, presetOptions: T): Preset;
@ -195,38 +202,40 @@ export type BuildCLIOptions = BuildOptions & {
locale?: string;
};
export interface LoadContext {
export type LoadContext = {
siteDir: string;
generatedFilesDir: string;
siteConfig: DocusaurusConfig;
siteConfigPath: 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;
ssrTemplate: string;
codeTranslations: {[msgId: string]: string};
}
export interface InjectedHtmlTags {
headTags: string;
preBodyTags: string;
postBodyTags: string;
}
};
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
export interface Props extends LoadContext, InjectedHtmlTags {
readonly siteMetadata: DocusaurusSiteMetadata;
export type Props = LoadContext & {
readonly headTags: string;
readonly preBodyTags: string;
readonly postBodyTags: string;
readonly siteMetadata: SiteMetadata;
readonly routes: RouteConfig[];
readonly routesPaths: string[];
readonly plugins: LoadedPlugin[];
}
};
export interface PluginContentLoadedActions {
export type PluginContentLoadedActions = {
addRoute: (config: RouteConfig) => void;
createData: (name: string, data: string) => Promise<string>;
setGlobalData: (data: unknown) => void;
}
};
export type AllContent = {
[pluginName: string]: {
@ -237,7 +246,7 @@ export type AllContent = {
// TODO improve type (not exposed by postcss-loader)
export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]};
export interface Plugin<Content = unknown> {
export type Plugin<Content = unknown> = {
name: string;
loadContent?: () => Promise<Content>;
contentLoaded?: (args: {
@ -273,19 +282,56 @@ export interface Plugin<Content = unknown> {
// TODO before/afterDevServer implementation
// translations
getTranslationFiles?: (args: {content: Content}) => Promise<TranslationFiles>;
getTranslationFiles?: (args: {
content: Content;
}) => Promise<TranslationFile[]>;
getDefaultCodeTranslationMessages?: () => Promise<{[id: string]: string}>;
translateContent?: (args: {
content: Content; // the content loaded by this plugin instance
translationFiles: TranslationFiles;
/** The content loaded by this plugin instance. */
content: Content;
translationFiles: TranslationFile[];
}) => Content;
translateThemeConfig?: (args: {
themeConfig: ThemeConfig;
translationFiles: TranslationFiles;
translationFiles: TranslationFile[];
}) => 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 version: PluginVersionInformation;
/**
@ -294,8 +340,8 @@ export type InitializedPlugin<Content = unknown> = Plugin<Content> & {
readonly path: string;
};
export type LoadedPlugin<Content = unknown> = InitializedPlugin<Content> & {
readonly content: Content;
export type LoadedPlugin = InitializedPlugin & {
readonly content: unknown;
};
export type SwizzleAction = 'eject' | 'wrap';
@ -314,9 +360,7 @@ export type SwizzleConfig = {
};
export type PluginModule = {
<Options, Content>(context: LoadContext, options: Options):
| Plugin<Content>
| Promise<Plugin<Content>>;
(context: LoadContext, options: unknown): Plugin | Promise<Plugin>;
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
@ -328,11 +372,11 @@ export type ImportedPluginModule = PluginModule & {
default?: PluginModule;
};
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
export type ConfigureWebpackFn = Plugin['configureWebpack'];
export type ConfigureWebpackFnMergeStrategy = {
[key: string]: CustomizeRuleString;
};
export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
export type ConfigurePostCssFn = Plugin['configurePostCss'];
export type PluginOptions = {id?: string} & {[key: string]: unknown};
@ -342,10 +386,10 @@ export type PluginConfig =
| [PluginModule, PluginOptions]
| PluginModule;
export interface ChunkRegistry {
export type ChunkRegistry = {
loader: string;
modulePath: string;
}
};
export type Module =
| {
@ -355,15 +399,15 @@ export type Module =
}
| string;
export interface RouteModule {
export type RouteModule = {
[module: string]: Module | RouteModule | RouteModule[];
}
};
export interface ChunkNames {
export type ChunkNames = {
[name: string]: string | null | ChunkNames | ChunkNames[];
}
};
export interface RouteConfig {
export type RouteConfig = {
path: string;
component: string;
modules?: RouteModule;
@ -371,25 +415,25 @@ export interface RouteConfig {
exact?: boolean;
priority?: number;
[propName: string]: unknown;
}
};
export interface RouteContext {
export type RouteContext = {
/**
* Plugin-specific context data.
*/
data?: object | undefined;
}
};
/**
* Top-level plugin routes automatically add some context data to the route.
* This permits us to know which plugin is handling the current route.
*/
export interface PluginRouteContext extends RouteContext {
export type PluginRouteContext = RouteContext & {
plugin: {
id: string;
name: string;
};
}
};
export type Route = {
readonly path: string;
@ -398,12 +442,14 @@ export type 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;
}
};
export interface ConfigureWebpackUtils {
export type ConfigureWebpackUtils = {
getStyleLoaders: (
isServer: boolean,
cssOptions: {
@ -414,23 +460,19 @@ export interface ConfigureWebpackUtils {
isServer: boolean;
babelOptions?: {[key: string]: unknown};
}) => RuleSetRule;
}
};
interface HtmlTagObject {
type HtmlTagObject = {
/**
* Attributes of the html tag
* E.g. `{'disabled': true, 'value': 'demo', 'rel': 'preconnect'}`
* Attributes of the html tag.
* E.g. `{ disabled: true, value: "demo", rel: "preconnect" }`
*/
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;
/**
* The inner HTML
*/
/** The inner HTML */
innerHTML?: string;
}
};
export type ValidationSchema<T> = Joi.ObjectSchema<T>;
@ -444,10 +486,10 @@ export type OptionValidationContext<T, U> = {
options: T;
};
export interface ThemeConfigValidationContext<T> {
export type ThemeConfigValidationContext<T> = {
validate: Validate<T, T>;
themeConfig: Partial<T>;
}
};
export type TOCItem = {
readonly value: string;

View file

@ -74,7 +74,6 @@
"html-tags": "^3.1.0",
"html-webpack-plugin": "^5.5.0",
"import-fresh": "^3.3.0",
"is-root": "^2.1.0",
"leven": "^3.1.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.6.0",

View file

@ -61,7 +61,8 @@ export async function build(
throw err;
}
}
const context = await loadContext(siteDir, {
const context = await loadContext({
siteDir,
customOutDir: cliOptions.outDir,
customConfigFilePath: cliOptions.config,
locale: cliOptions.locale,
@ -109,7 +110,8 @@ async function buildLocale({
process.env.NODE_ENV = 'production';
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
const props: Props = await load(siteDir, {
const props: Props = await load({
siteDir,
customOutDir: cliOptions.outDir,
customConfigFilePath: cliOptions.config,
locale,

View file

@ -5,7 +5,7 @@
* 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 {DEFAULT_PORT} from '@docusaurus/utils';

View file

@ -38,7 +38,8 @@ export async function deploy(
siteDir: string,
cliOptions: Partial<BuildCLIOptions> = {},
): Promise<void> {
const {outDir, siteConfig, siteConfigPath} = await loadContext(siteDir, {
const {outDir, siteConfig, siteConfigPath} = await loadContext({
siteDir,
customConfigFilePath: cliOptions.config,
customOutDir: cliOptions.outDir,
});

View file

@ -13,7 +13,7 @@ export async function externalCommand(
cli: CommanderStatic,
siteDir: string,
): Promise<void> {
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugins = await initPlugins(context);
// Plugin Lifecycle - extendCli.

View file

@ -9,7 +9,7 @@ import http from 'http';
import serveHandler from 'serve-handler';
import logger from '@docusaurus/logger';
import path from 'path';
import {loadSiteConfig} from '../server';
import {loadSiteConfig} from '../server/config';
import {build} from './build';
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
import type {ServeCLIOptions} from '@docusaurus/types';

View file

@ -37,7 +37,8 @@ export async function start(
logger.info('Starting the development server...');
function loadSite() {
return load(siteDir, {
return load({
siteDir,
customConfigFilePath: cliOptions.config,
locale: cliOptions.locale,
localizePath: undefined, // should this be configurable?

View file

@ -12,8 +12,8 @@ import type {
InitializedPlugin,
SwizzleAction,
SwizzleActionStatus,
NormalizedPluginConfig,
} from '@docusaurus/types';
import type {NormalizedPluginConfig} from '../../server/plugins/init';
export const SwizzleActions: SwizzleAction[] = ['wrap', 'eject'];

View file

@ -6,25 +6,20 @@
*/
import {loadContext} from '../../server';
import {initPlugins, normalizePluginConfigs} from '../../server/plugins/init';
import {initPlugins} from '../../server/plugins/init';
import {loadPluginConfigs} from '../../server/plugins/configs';
import type {SwizzleContext} from './common';
export async function initSwizzleContext(
siteDir: string,
): Promise<SwizzleContext> {
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugins = await initPlugins(context);
const pluginConfigs = await loadPluginConfigs(context);
const pluginsNormalized = await normalizePluginConfigs(
pluginConfigs,
context.siteConfigPath,
);
return {
plugins: plugins.map((plugin, pluginIndex) => ({
plugin: pluginsNormalized[pluginIndex]!,
plugin: pluginConfigs[pluginIndex]!,
instance: plugin,
})),
};

View file

@ -35,7 +35,7 @@ async function transformMarkdownFile(
* transformed
*/
async function getPathsToWatch(siteDir: string): Promise<string[]> {
const context = await loadContext(siteDir);
const context = await loadContext({siteDir});
const plugins = await initPlugins(context);
return plugins.flatMap((plugin) => plugin?.getPathsToWatch?.() ?? []);
}

View file

@ -76,7 +76,8 @@ export async function writeTranslations(
siteDir: string,
options: WriteTranslationsOptions & ConfigOptions & {locale?: string},
): Promise<void> {
const context = await loadContext(siteDir, {
const context = await loadContext({
siteDir,
customConfigFilePath: options.config,
locale: options.locale,
});

View file

@ -1,161 +1,162 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loadConfig website with incomplete siteConfig 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`] = `
exports[`loadSiteConfig website with valid async config 1`] = `
{
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"locales": [
"en",
"siteConfig": {
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"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,
"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",
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/configAsync.config.js",
}
`;
exports[`loadConfig website with valid async config creator function 1`] = `
exports[`loadSiteConfig website with valid async config creator function 1`] = `
{
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"locales": [
"en",
"siteConfig": {
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"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,
"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",
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/createConfigAsync.config.js",
}
`;
exports[`loadConfig website with valid config creator function 1`] = `
exports[`loadSiteConfig website with valid config creator function 1`] = `
{
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"locales": [
"en",
"siteConfig": {
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"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,
"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",
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/configs/createConfig.config.js",
}
`;
exports[`loadConfig website with valid siteConfig 1`] = `
exports[`loadSiteConfig website with valid siteConfig 1`] = `
{
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"favicon": "img/docusaurus.ico",
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"locales": [
"en",
"siteConfig": {
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"favicon": "img/docusaurus.ico",
"i18n": {
"defaultLocale": "en",
"localeConfigs": {},
"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,
"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",
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js",
}
`;

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
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`] = `
{
"registry": {

View file

@ -6,86 +6,76 @@
*/
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 () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'simple-site',
'docusaurus.config.js',
);
const config = await loadConfig(siteDir);
const config = await loadSiteConfig({
siteDir: path.join(__dirname, '__fixtures__', 'simple-site'),
});
expect(config).toMatchSnapshot();
expect(config).not.toEqual({});
});
it('website with valid config creator function', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'configs',
'createConfig.config.js',
);
const config = await loadConfig(siteDir);
const config = await loadSiteConfig({
siteDir,
customConfigFilePath: 'createConfig.config.js',
});
expect(config).toMatchSnapshot();
expect(config).not.toEqual({});
});
it('website with valid async config', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'configs',
'configAsync.config.js',
);
const config = await loadConfig(siteDir);
const config = await loadSiteConfig({
siteDir,
customConfigFilePath: 'configAsync.config.js',
});
expect(config).toMatchSnapshot();
expect(config).not.toEqual({});
});
it('website with valid async config creator function', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'configs',
'createConfigAsync.config.js',
);
const config = await loadConfig(siteDir);
const config = await loadSiteConfig({
siteDir,
customConfigFilePath: 'createConfigAsync.config.js',
});
expect(config).toMatchSnapshot();
expect(config).not.toEqual({});
});
it('website with incomplete siteConfig', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'bad-site',
'docusaurus.config.js',
);
await expect(loadConfig(siteDir)).rejects.toThrowErrorMatchingSnapshot();
await expect(
loadSiteConfig({
siteDir: path.join(__dirname, '__fixtures__', 'bad-site'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"\\"url\\" is required
"
`);
});
it('website with useless field (wrong field) in siteConfig', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'wrong-site',
'docusaurus.config.js',
);
await expect(loadConfig(siteDir)).rejects.toThrowErrorMatchingSnapshot();
await expect(
loadSiteConfig({
siteDir: path.join(__dirname, '__fixtures__', 'wrong-site'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"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"
`);
});
it('website with no siteConfig', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'nonExisting',
'docusaurus.config.js',
);
await expect(loadConfig(siteDir)).rejects.toThrowError(
/Config file at ".*?__fixtures__[/\\]nonExisting[/\\]docusaurus.config.js" not found.$/,
await expect(
loadSiteConfig({
siteDir: path.join(__dirname, '__fixtures__', 'nonExisting'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Config file at \\"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__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.
*/
import {loadRoutes} from '../routes';
import {jest} from '@jest/globals';
import {loadRoutes, handleDuplicateRoutes} from '../routes';
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', () => {
it('loads nested route config', async () => {
const nestedRouteConfig: RouteConfig = {
@ -44,7 +87,7 @@ describe('loadRoutes', () => {
],
};
await expect(
loadRoutes([nestedRouteConfig], '/'),
loadRoutes([nestedRouteConfig], '/', 'ignore'),
).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 () => {
@ -87,7 +132,7 @@ describe('loadRoutes', () => {
component: 'hello/world.js',
} as RouteConfig;
await expect(loadRoutes([routeConfigWithoutPath], '/')).rejects
await expect(loadRoutes([routeConfigWithoutPath], '/', 'ignore')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Invalid route config: path must be a string and component is required.
{\\"component\\":\\"hello/world.js\\"}"
@ -97,8 +142,8 @@ describe('loadRoutes', () => {
path: '/hello/world',
} as RouteConfig;
await expect(loadRoutes([routeConfigWithoutComponent], '/')).rejects
.toThrowErrorMatchingInlineSnapshot(`
await expect(loadRoutes([routeConfigWithoutComponent], '/', 'ignore'))
.rejects.toThrowErrorMatchingInlineSnapshot(`
"Invalid route config: path must be a string and component is required.
{\\"path\\":\\"/hello/world\\"}"
`);
@ -110,6 +155,8 @@ describe('loadRoutes', () => {
component: 'hello/world.js',
} 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) {
case 'custom':
return load(customSite);
return load({siteDir: customSite});
case 'simple':
default:
return load(simpleSite);
return load({siteDir: simpleSite});
}
}

View file

@ -5,19 +5,11 @@
* 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 detect from 'detect-port';
import isRoot from 'is-root';
import logger from '@docusaurus/logger';
import prompts from 'prompts';
const isInteractive = process.stdout.isTTY;
const execOptions = {
encoding: 'utf8' as const,
stdio: [
@ -72,10 +64,11 @@ function getProcessForPort(port: number): string | null {
}
/**
* Detects if program is running on port and prompts user
* to choose another if port is already being used
* Detects if program is running on port, and prompts user to choose another if
* 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,
defaultPort: number,
): Promise<number | null> {
@ -84,8 +77,10 @@ export default async function choosePort(
if (port === defaultPort) {
return port;
}
const isRoot = process.getuid?.() === 0;
const isInteractive = process.stdout.isTTY;
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.`
: `Something is already running on port ${defaultPort}.`;
if (!isInteractive) {

View file

@ -8,7 +8,11 @@
import path from 'path';
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(
(plugin) =>
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.
*/
import path from 'path';
import fs from 'fs-extra';
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';
export async function loadConfig(
configPath: string,
): Promise<DocusaurusConfig> {
if (!(await fs.pathExists(configPath))) {
throw new Error(`Config file at "${configPath}" not found.`);
export async function loadSiteConfig({
siteDir,
customConfigFilePath,
}: {
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
| Partial<DocusaurusConfig>
| Promise<Partial<DocusaurusConfig>>
| (() => Partial<DocusaurusConfig>)
| (() => Promise<Partial<DocusaurusConfig>>);
const importedConfig = importFresh(siteConfigPath);
const loadedConfig =
typeof importedConfig === 'function'
? 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 _ from 'lodash';
import type {
InjectedHtmlTags,
Props,
HtmlTagObject,
HtmlTags,
LoadedPlugin,
@ -62,7 +62,13 @@ function createHtmlTagsString(tags: HtmlTags | undefined): string {
.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(
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
);

View file

@ -8,6 +8,7 @@
import {getLangDir} from 'rtl-detect';
import logger from '@docusaurus/logger';
import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
import type {LoadContextOptions} from './index';
function getDefaultLocaleLabel(locale: string) {
const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of(
@ -28,7 +29,7 @@ export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
export async function loadI18n(
config: DocusaurusConfig,
options: {locale?: string},
options: Pick<LoadContextOptions, 'locale'>,
): Promise<I18n> {
const {i18n: i18nConfig} = config;

View file

@ -15,14 +15,13 @@ import {
} from '@docusaurus/utils';
import _ from 'lodash';
import path from 'path';
import {loadSiteConfig} from './config';
import ssrDefaultTemplate from '../webpack/templates/ssr.html.template';
import {loadClientModules} from './clientModules';
import {loadConfig} from './config';
import {loadPlugins} from './plugins';
import {loadRoutes} from './routes';
import {loadHtmlTags} from './htmlTags';
import {loadSiteMetadata} from './siteMetadata';
import {handleDuplicateRoutes} from './duplicateRoutes';
import {loadI18n} from './i18n';
import {
readCodeTranslationFileContent,
@ -31,45 +30,39 @@ import {
import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types';
export type LoadContextOptions = {
/** Usually the CWD; can be overridden with command argument. */
siteDir: string;
/** Can be customized with `--out-dir` option */
customOutDir?: string;
/** Can be customized with `--config` option */
customConfigFilePath?: string;
/** Default is `i18n.defaultLocale` */
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,
customConfigFilePath,
}: {
siteDir: string;
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};
}
/**
* Loading context is the very first step in site building. Its options are
* directly acquired from CLI options. It mainly loads `siteConfig` and the i18n
* context (which includes code translations). The `LoadContext` will be passed
* to plugin constructors.
*/
export async function loadContext(
siteDir: string,
options: LoadContextOptions = {},
options: LoadContextOptions,
): Promise<LoadContext> {
const {customOutDir, locale, customConfigFilePath} = options;
const {siteDir, customOutDir, locale, customConfigFilePath} = options;
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({
siteDir,
customConfigFilePath,
});
const {ssrTemplate} = initialSiteConfig;
const baseOutDir = path.resolve(
siteDir,
customOutDir ?? DEFAULT_BUILD_DIR_NAME,
);
const i18n = await loadI18n(initialSiteConfig, {locale});
@ -80,7 +73,7 @@ export async function loadContext(
pathType: 'url',
});
const outDir = localizePath({
path: baseOutDir,
path: path.resolve(siteDir, customOutDir ?? DEFAULT_BUILD_DIR_NAME),
i18n,
options,
pathType: 'fs',
@ -106,19 +99,22 @@ export async function loadContext(
siteConfig,
siteConfigPath,
outDir,
baseUrl, // TODO to remove: useless, there's already siteConfig.baseUrl! (and yes, it's the same value, cf code above)
baseUrl,
i18n,
ssrTemplate: ssrTemplate ?? ssrDefaultTemplate,
ssrTemplate: siteConfig.ssrTemplate ?? ssrDefaultTemplate,
codeTranslations,
};
}
export async function load(
siteDir: string,
options: LoadContextOptions = {},
): Promise<Props> {
// Context.
const context: LoadContext = await loadContext(siteDir, options);
/**
* This is the crux of the Docusaurus server-side. It reads everything it needs
* code translations, config file, plugin modules... Plugins then use their
* lifecycles to generate content and other data. It is side-effect-ful because
* it generates temp files in the `.docusaurus` folder for the bundler.
*/
export async function load(options: LoadContextOptions): Promise<Props> {
const {siteDir} = options;
const context = await loadContext(options);
const {
generatedFilesDir,
siteConfig,
@ -127,16 +123,28 @@ export async function load(
baseUrl,
i18n,
ssrTemplate,
codeTranslations,
codeTranslations: siteCodeTranslations,
} = context;
// Plugins.
const {plugins, pluginsRouteConfigs, globalData, themeConfigTranslated} =
await loadPlugins(context);
// Side-effect to replace the untranslated themeConfig by the translated one
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(
generatedFilesDir,
'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(
generatedFilesDir,
'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(
generatedFilesDir,
'registry.js',
@ -220,19 +219,12 @@ ${Object.entries(registry)
JSON.stringify(i18n, null, 2),
);
const codeTranslationsWithFallbacks: {[msgId: string]: string} = {
...(await getPluginsDefaultCodeTranslationMessages(plugins)),
...codeTranslations,
};
const genCodeTranslations = generate(
generatedFilesDir,
'codeTranslations.json',
JSON.stringify(codeTranslationsWithFallbacks, null, 2),
JSON.stringify(codeTranslations, null, 2),
);
// Version metadata.
const siteMetadata = await loadSiteMetadata({plugins, siteDir});
const genSiteMetadata = generate(
generatedFilesDir,
'site-metadata.json',
@ -252,7 +244,7 @@ ${Object.entries(registry)
genCodeTranslations,
]);
const props: Props = {
return {
siteConfig,
siteConfigPath,
siteMetadata,
@ -270,6 +262,4 @@ ${Object.entries(registry)
ssrTemplate,
codeTranslations,
};
return props;
}

View file

@ -11,9 +11,9 @@ import {loadContext, type LoadContextOptions} from '../../index';
import {initPlugins} from '../init';
describe('initPlugins', () => {
async function loadSite(options: LoadContextOptions = {}) {
async function loadSite(options: Omit<LoadContextOptions, 'siteDir'> = {}) {
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);
return {siteDir, context, plugins};

View file

@ -6,28 +6,97 @@
*/
import {createRequire} from 'module';
import importFresh from 'import-fresh';
import {loadPresets} from './presets';
import {resolveModuleName} from '../moduleShorthand';
import type {LoadContext, PluginConfig} from '@docusaurus/types';
import {resolveModuleName} from './moduleShorthand';
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(
context: LoadContext,
): Promise<PluginConfig[]> {
): Promise<NormalizedPluginConfig[]> {
const preset = await loadPresets(context);
const {siteConfig, siteConfigPath} = context;
const require = createRequire(siteConfigPath);
const pluginRequire = createRequire(siteConfigPath);
function normalizeShorthand(
pluginConfig: PluginConfig,
pluginType: 'plugin' | 'theme',
): PluginConfig {
if (typeof pluginConfig === 'string') {
return resolveModuleName(pluginConfig, require, pluginType);
return resolveModuleName(pluginConfig, pluginRequire, pluginType);
} else if (
Array.isArray(pluginConfig) &&
typeof pluginConfig[0] === 'string'
) {
return [
resolveModuleName(pluginConfig[0], require, pluginType),
resolveModuleName(pluginConfig[0], pluginRequire, pluginType),
pluginConfig[1] ?? {},
];
}
@ -45,11 +114,20 @@ export async function loadPluginConfigs(
const standaloneThemes = siteConfig.themes.map((theme) =>
normalizeShorthand(theme, 'theme'),
);
return [
const pluginConfigs = [
...preset.plugins,
...preset.themes,
// Site config should be the highest priority.
...standalonePlugins,
...standaloneThemes,
];
return Promise.all(
pluginConfigs.map((pluginConfig) =>
normalizePluginConfig(
pluginConfig,
context.siteConfigPath,
pluginRequire,
),
),
);
}

View file

@ -14,7 +14,6 @@ import type {
RouteConfig,
AllContent,
GlobalData,
TranslationFiles,
ThemeConfig,
LoadedPlugin,
InitializedPlugin,
@ -27,6 +26,10 @@ import _ from 'lodash';
import {localizePluginTranslationFile} from '../translations/translations';
import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
/**
* Initializes the plugins, runs `loadContent`, `translateContent`,
* `contentLoaded`, and `translateThemeConfig`.
*/
export async function loadPlugins(context: LoadContext): Promise<{
plugins: LoadedPlugin[];
pluginsRouteConfigs: RouteConfig[];
@ -52,32 +55,28 @@ export async function loadPlugins(context: LoadContext): Promise<{
}),
);
type ContentLoadedTranslatedPlugin = LoadedPlugin & {
translationFiles: TranslationFiles;
};
const contentLoadedTranslatedPlugins: ContentLoadedTranslatedPlugin[] =
await Promise.all(
loadedPlugins.map(async (contentLoadedPlugin) => {
const translationFiles =
(await contentLoadedPlugin?.getTranslationFiles?.({
content: contentLoadedPlugin.content,
})) ?? [];
const localizedTranslationFiles = await Promise.all(
translationFiles.map((translationFile) =>
localizePluginTranslationFile({
locale: context.i18n.currentLocale,
siteDir: context.siteDir,
translationFile,
plugin: contentLoadedPlugin,
}),
),
);
return {
...contentLoadedPlugin,
translationFiles: localizedTranslationFiles,
};
}),
);
const contentLoadedTranslatedPlugins = await Promise.all(
loadedPlugins.map(async (plugin) => {
const translationFiles =
(await plugin?.getTranslationFiles?.({
content: plugin.content,
})) ?? [];
const localizedTranslationFiles = await Promise.all(
translationFiles.map((translationFile) =>
localizePluginTranslationFile({
locale: context.i18n.currentLocale,
siteDir: context.siteDir,
translationFile,
plugin,
}),
),
);
return {
...plugin,
translationFiles: localizedTranslationFiles,
};
}),
);
const allContent: AllContent = _.chain(loadedPlugins)
.groupBy((item) => item.name)
@ -174,9 +173,6 @@ export async function loadPlugins(context: LoadContext): Promise<{
);
// 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(
contentLoadedTranslatedPlugins.map(async (plugin) => {
if (!plugin.routesLoaded) {
@ -197,28 +193,24 @@ export async function loadPlugins(context: LoadContext): Promise<{
sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl);
// Apply each plugin one after the other to translate the theme config
function translateThemeConfig(
untranslatedThemeConfig: ThemeConfig,
): ThemeConfig {
return contentLoadedTranslatedPlugins.reduce(
(currentThemeConfig, plugin) => {
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
themeConfig: currentThemeConfig,
translationFiles: plugin.translationFiles,
});
return {
...currentThemeConfig,
...translatedThemeConfigSlice,
};
},
untranslatedThemeConfig,
);
}
const themeConfigTranslated = contentLoadedTranslatedPlugins.reduce(
(currentThemeConfig, plugin) => {
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
themeConfig: currentThemeConfig,
translationFiles: plugin.translationFiles,
});
return {
...currentThemeConfig,
...translatedThemeConfigSlice,
};
},
context.siteConfig.themeConfig,
);
return {
plugins: loadedPlugins,
pluginsRouteConfigs,
globalData,
themeConfigTranslated: translateThemeConfig(context.siteConfig.themeConfig),
themeConfigTranslated,
};
}

View file

@ -7,15 +7,13 @@
import {createRequire} from 'module';
import path from 'path';
import importFresh from 'import-fresh';
import type {
PluginVersionInformation,
ImportedPluginModule,
LoadContext,
PluginModule,
PluginConfig,
PluginOptions,
InitializedPlugin,
NormalizedPluginConfig,
} from '@docusaurus/types';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {getPluginVersion} from '../siteMetadata';
@ -26,89 +24,6 @@ import {
} from '@docusaurus/utils-validation';
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(
normalizedPluginConfig: NormalizedPluginConfig,
): PluginModule['validateOptions'] {
@ -135,17 +50,17 @@ function getThemeValidationFunction(
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(
context: LoadContext,
): Promise<InitializedPlugin[]> {
// We need to resolve plugins from the perspective of the siteDir, since the
// siteDir's package.json declares the dependency on these plugins.
// We need to resolve plugins from the perspective of the site config, as if
// we are using `require.resolve` on those module names.
const pluginRequire = createRequire(context.siteConfigPath);
const pluginConfigs = await loadPluginConfigs(context);
const pluginConfigsNormalized = await normalizePluginConfigs(
pluginConfigs,
context.siteConfigPath,
);
async function doGetPluginVersion(
normalizedPluginConfig: NormalizedPluginConfig,
@ -221,7 +136,7 @@ export async function initPlugins(
}
const plugins: InitializedPlugin[] = await Promise.all(
pluginConfigsNormalized.map(initializePlugin),
pluginConfigs.map(initializePlugin),
);
ensureUniquePluginInstanceIds(plugins);

View file

@ -9,8 +9,10 @@ import _ from 'lodash';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
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(
plugins: InitializedPlugin[],
): void {

View file

@ -11,15 +11,19 @@ import type {
LoadContext,
PluginConfig,
ImportedPresetModule,
DocusaurusConfig,
} from '@docusaurus/types';
import {resolveModuleName} from '../moduleShorthand';
import {resolveModuleName} from './moduleShorthand';
export async function loadPresets(context: LoadContext): Promise<{
plugins: PluginConfig[];
themes: PluginConfig[];
}> {
// We need to resolve presets from the perspective of the siteDir, since the
// siteDir's package.json declares the dependency on these presets.
/**
* Calls preset functions, aggregates each of their return values, and returns
* the plugin and theme configs.
*/
export async function loadPresets(
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 {presets} = context.siteConfig;

View file

@ -11,14 +11,17 @@ import {
removeSuffix,
simpleHash,
escapePath,
reportMessage,
} from '@docusaurus/utils';
import {stringify} from 'querystring';
import {getAllFinalRoutes} from './utils';
import type {
ChunkRegistry,
Module,
RouteConfig,
RouteModule,
ChunkNames,
ReportingSeverity,
} from '@docusaurus/types';
type RegistryMap = {
@ -119,15 +122,107 @@ function getModulePath(target: Module): string {
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(
pluginsRouteConfigs: RouteConfig[],
baseUrl: string,
onDuplicateRoutes: ReportingSeverity,
): Promise<{
registry: {[chunkName: string]: ChunkRegistry};
routesConfig: string;
routesChunkNames: {[routePath: string]: ChunkNames};
routesPaths: string[];
}> {
handleDuplicateRoutes(pluginsRouteConfigs, onDuplicateRoutes);
const registry: {[chunkName: string]: ChunkRegistry} = {};
const routesPaths: string[] = [normalizeUrl([baseUrl, '404.html'])];
const routesChunkNames: {[routePath: string]: ChunkNames} = {};
@ -194,63 +289,3 @@ ${indent(NotFoundRouteCode)}
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 {
LoadedPlugin,
PluginVersionInformation,
DocusaurusSiteMetadata,
SiteMetadata,
} from '@docusaurus/types';
import fs from 'fs-extra';
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
// 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'};
}
@ -70,7 +71,7 @@ export async function getPluginVersion(
* @see https://github.com/facebook/docusaurus/issues/3371
* @see https://github.com/facebook/docusaurus/pull/3386
*/
function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) {
function checkDocusaurusPackagesVersion(siteMetadata: SiteMetadata) {
const {docusaurusVersion} = siteMetadata;
Object.entries(siteMetadata.pluginVersions).forEach(
([plugin, versionInfo]) => {
@ -96,8 +97,8 @@ export async function loadSiteMetadata({
}: {
plugins: LoadedPlugin[];
siteDir: string;
}): Promise<DocusaurusSiteMetadata> {
const siteMetadata: DocusaurusSiteMetadata = {
}): Promise<SiteMetadata> {
const siteMetadata: SiteMetadata = {
docusaurusVersion: (await getPackageJsonVersion(
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?
function getTranslationsDirPath(context: TranslationContext): string {
return path.join(context.siteDir, I18N_DIR_NAME);
}
export function getTranslationsLocaleDirPath(
context: TranslationContext,
): string {
return path.join(getTranslationsDirPath(context), context.locale);
return path.join(context.siteDir, I18N_DIR_NAME, context.locale);
}
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",
}
`;
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 path from 'path';
import {
excludeJS,
clientDir,
getDocusaurusAliases,
createBaseConfig,
} from '../base';
import {excludeJS, clientDir, createBaseConfig} from '../base';
import * as utils from '@docusaurus/utils/lib/webpackUtils';
import {posixPath} from '@docusaurus/utils';
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', () => {
const props: Props = {
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.
*/
import path from 'path';
import fs from 'fs-extra';
import {themeAlias, sortAliases} from '../alias';
import path from 'path';
import {
loadThemeAliases,
loadDocusaurusAliases,
sortAliases,
createAliasesForTheme,
} from '../index';
describe('sortAliases', () => {
// https://github.com/facebook/docusaurus/issues/6878
@ -53,11 +58,11 @@ describe('sortAliases', () => {
});
});
describe('themeAlias', () => {
it('valid themePath 1 with components', async () => {
describe('createAliasesForTheme', () => {
it('creates aliases for themePath 1 with components', async () => {
const fixtures = path.join(__dirname, '__fixtures__');
const themePath = path.join(fixtures, 'theme-1');
const alias = await themeAlias(themePath, true);
const alias = await createAliasesForTheme(themePath, true);
// Testing entries, because order matters!
expect(Object.entries(alias)).toEqual(
Object.entries({
@ -70,10 +75,10 @@ describe('themeAlias', () => {
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 themePath = path.join(fixtures, 'theme-1');
const alias = await themeAlias(themePath, false);
const alias = await createAliasesForTheme(themePath, false);
// Testing entries, because order matters!
expect(Object.entries(alias)).toEqual(
Object.entries({
@ -84,10 +89,10 @@ describe('themeAlias', () => {
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 themePath = path.join(fixtures, 'theme-2');
const alias = await themeAlias(themePath, true);
const alias = await createAliasesForTheme(themePath, true);
// Testing entries, because order matters!
expect(Object.entries(alias)).toEqual(
Object.entries({
@ -127,10 +132,10 @@ describe('themeAlias', () => {
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 themePath = path.join(fixtures, 'theme-2');
const alias = await themeAlias(themePath, false);
const alias = await createAliasesForTheme(themePath, false);
// Testing entries, because order matters!
expect(Object.entries(alias)).toEqual(
Object.entries({
@ -151,26 +156,51 @@ describe('themeAlias', () => {
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 themePath = path.join(fixtures, 'empty-theme');
await fs.ensureDir(themePath);
const alias = await themeAlias(themePath, true);
const alias = await createAliasesForTheme(themePath, true);
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 themePath = path.join(fixtures, 'empty-theme');
await fs.ensureDir(themePath);
const alias = await themeAlias(themePath, false);
const alias = await createAliasesForTheme(themePath, false);
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 themePath = path.join(fixtures, '__noExist__');
const alias = await themeAlias(themePath, true);
const alias = await createAliasesForTheme(themePath, true);
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,
getMinimizer,
} from './utils';
import {loadPluginsThemeAliases} from '../server/themes';
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils';
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(
props: Props,
isServer: boolean,
@ -92,7 +70,7 @@ export async function createBaseConfig(
const name = isServer ? 'server' : 'client';
const mode = isProd ? 'production' : 'development';
const themeAliases = await loadPluginsThemeAliases({siteDir, plugins});
const themeAliases = await loadThemeAliases({siteDir, plugins});
return {
mode,
@ -156,11 +134,7 @@ export async function createBaseConfig(
alias: {
'@site': siteDir,
'@generated': generatedFilesDir,
// 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()),
...(await loadDocusaurusAliases()),
...themeAliases,
},
// 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: 'synthetic'};
interface DocusaurusSiteMetadata {
interface SiteMetadata {
readonly docusaurusVersion: string;
readonly siteVersion?: string;
readonly pluginVersions: Record<string, PluginVersionInformation>;
@ -361,7 +361,7 @@ interface I18n {
interface DocusaurusContext {
siteConfig: DocusaurusConfig;
siteMetadata: DocusaurusSiteMetadata;
siteMetadata: SiteMetadata;
globalData: Record<string, unknown>;
i18n: I18n;
codeTranslations: Record<string, string>;