mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
refactor(core): reorganize files (#7042)
* refactor(core): reorganize files * fix types
This commit is contained in:
parent
85a79fd9b9
commit
5fb09a2946
61 changed files with 1089 additions and 1028 deletions
|
@ -20,9 +20,9 @@ declare module '@generated/docusaurus.config' {
|
|||
}
|
||||
|
||||
declare module '@generated/site-metadata' {
|
||||
import type {DocusaurusSiteMetadata} from '@docusaurus/types';
|
||||
import type {SiteMetadata} from '@docusaurus/types';
|
||||
|
||||
const siteMetadata: DocusaurusSiteMetadata;
|
||||
const siteMetadata: SiteMetadata;
|
||||
export = siteMetadata;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
206
packages/docusaurus-types/src/index.d.ts
vendored
206
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -10,7 +10,11 @@ import type {CustomizeRuleString} from 'webpack-merge/dist/types';
|
|||
import type {CommanderStatic} from 'commander';
|
||||
import type {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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
|
|
|
@ -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?.() ?? []);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`handleDuplicateRoutes works 1`] = `
|
||||
"Duplicate routes found!
|
||||
- Attempting to create page at /search, but a page already exists at this route.
|
||||
- Attempting to create page at /sameDoc, but a page already exists at this route.
|
||||
- Attempting to create page at /, but a page already exists at this route.
|
||||
- Attempting to create page at /, but a page already exists at this route.
|
||||
This could lead to non-deterministic routing behavior."
|
||||
`;
|
|
@ -1,5 +1,14 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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": {
|
||||
|
|
|
@ -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."`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {jest} from '@jest/globals';
|
||||
import {handleDuplicateRoutes} from '../duplicateRoutes';
|
||||
import type {RouteConfig} from '@docusaurus/types';
|
||||
|
||||
const routes: RouteConfig[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: '',
|
||||
routes: [
|
||||
{path: '/search', component: ''},
|
||||
{path: '/sameDoc', component: ''},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '',
|
||||
routes: [
|
||||
{path: '/search', component: ''},
|
||||
{path: '/sameDoc', component: ''},
|
||||
{path: '/uniqueDoc', component: ''},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '',
|
||||
},
|
||||
];
|
||||
|
||||
describe('handleDuplicateRoutes', () => {
|
||||
it('works', () => {
|
||||
expect(() => {
|
||||
handleDuplicateRoutes(routes, 'throw');
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
const consoleMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
handleDuplicateRoutes(routes, 'ignore');
|
||||
expect(consoleMock).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -5,9 +5,52 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
|
@ -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)) ??
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {ReportingSeverity, RouteConfig} from '@docusaurus/types';
|
||||
import {reportMessage} from '@docusaurus/utils';
|
||||
import {getAllFinalRoutes} from './utils';
|
||||
|
||||
function getAllDuplicateRoutes(pluginsRouteConfigs: RouteConfig[]): string[] {
|
||||
const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map(
|
||||
(routeConfig) => routeConfig.path,
|
||||
);
|
||||
const seenRoutes = new Set<string>();
|
||||
return allRoutes.filter((route) => {
|
||||
if (seenRoutes.has(route)) {
|
||||
return true;
|
||||
}
|
||||
seenRoutes.add(route);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function getDuplicateRoutesMessage(allDuplicateRoutes: string[]): string {
|
||||
const message = allDuplicateRoutes
|
||||
.map(
|
||||
(duplicateRoute) =>
|
||||
`- Attempting to create page at ${duplicateRoute}, but a page already exists at this route.`,
|
||||
)
|
||||
.join('\n');
|
||||
return message;
|
||||
}
|
||||
|
||||
export function handleDuplicateRoutes(
|
||||
pluginsRouteConfigs: RouteConfig[],
|
||||
onDuplicateRoutes: ReportingSeverity,
|
||||
): void {
|
||||
if (onDuplicateRoutes === 'ignore') {
|
||||
return;
|
||||
}
|
||||
const duplicatePaths: string[] = getAllDuplicateRoutes(pluginsRouteConfigs);
|
||||
const message: string = getDuplicateRoutesMessage(duplicatePaths);
|
||||
if (message) {
|
||||
const finalMessage = `Duplicate routes found!
|
||||
${message}
|
||||
This could lead to non-deterministic routing behavior.`;
|
||||
reportMessage(finalMessage, onDuplicateRoutes);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import voidHtmlTags from 'html-tags/void';
|
|||
import escapeHTML from 'escape-html';
|
||||
import _ 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}) ?? {},
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
))!,
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {loadThemeAliases} from '../index';
|
||||
|
||||
describe('loadThemeAliases', () => {
|
||||
it('next alias can override the previous alias', async () => {
|
||||
const fixtures = path.join(__dirname, '__fixtures__');
|
||||
const theme1Path = path.join(fixtures, 'theme-1');
|
||||
const theme2Path = path.join(fixtures, 'theme-2');
|
||||
|
||||
const alias = await loadThemeAliases([theme1Path, theme2Path], []);
|
||||
|
||||
// Testing entries, because order matters!
|
||||
expect(Object.entries(alias)).toEqual(
|
||||
Object.entries({
|
||||
'@theme-init/Layout': path.join(theme1Path, 'Layout.js'),
|
||||
|
||||
'@theme-original/Footer': path.join(theme1Path, 'Footer/index.js'),
|
||||
'@theme-original/Layout': path.join(theme2Path, 'Layout/index.js'),
|
||||
'@theme-original/Navbar': path.join(theme2Path, 'Navbar.js'),
|
||||
'@theme-original/NavbarItem/NestedNavbarItem': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/NestedNavbarItem/index.js',
|
||||
),
|
||||
'@theme-original/NavbarItem/SiblingNavbarItem': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/SiblingNavbarItem.js',
|
||||
),
|
||||
'@theme-original/NavbarItem/zzz': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/zzz.js',
|
||||
),
|
||||
'@theme-original/NavbarItem': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/index.js',
|
||||
),
|
||||
|
||||
'@theme/Footer': path.join(theme1Path, 'Footer/index.js'),
|
||||
'@theme/Layout': path.join(theme2Path, 'Layout/index.js'),
|
||||
'@theme/Navbar': path.join(theme2Path, 'Navbar.js'),
|
||||
'@theme/NavbarItem/NestedNavbarItem': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/NestedNavbarItem/index.js',
|
||||
),
|
||||
'@theme/NavbarItem/SiblingNavbarItem': path.join(
|
||||
theme2Path,
|
||||
'NavbarItem/SiblingNavbarItem.js',
|
||||
),
|
||||
'@theme/NavbarItem/zzz': path.join(theme2Path, 'NavbarItem/zzz.js'),
|
||||
'@theme/NavbarItem': path.join(theme2Path, 'NavbarItem/index.js'),
|
||||
}),
|
||||
);
|
||||
expect(alias).not.toEqual({});
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {fileToPath, posixPath, normalizeUrl, Globby} from '@docusaurus/utils';
|
||||
import type {ThemeAliases} from '@docusaurus/types';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Order of Webpack aliases is important because one alias can shadow another
|
||||
// This ensure @theme/NavbarItem alias is after @theme/NavbarItem/LocaleDropdown
|
||||
// See https://github.com/facebook/docusaurus/pull/3922
|
||||
// See https://github.com/facebook/docusaurus/issues/5382
|
||||
export function sortAliases(aliases: ThemeAliases): ThemeAliases {
|
||||
// Alphabetical order by default
|
||||
const entries = _.sortBy(Object.entries(aliases), ([alias]) => alias);
|
||||
// @theme/NavbarItem should be after @theme/NavbarItem/LocaleDropdown
|
||||
entries.sort(([alias1], [alias2]) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
alias1.includes(`${alias2}/`) ? -1 : alias2.includes(`${alias1}/`) ? 1 : 0,
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
export async function themeAlias(
|
||||
themePath: string,
|
||||
addOriginalAlias: boolean,
|
||||
): Promise<ThemeAliases> {
|
||||
if (!(await fs.pathExists(themePath))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const themeComponentFiles = await Globby(['**/*.{js,jsx,ts,tsx}'], {
|
||||
cwd: themePath,
|
||||
});
|
||||
|
||||
const aliases: ThemeAliases = {};
|
||||
|
||||
themeComponentFiles.forEach((relativeSource) => {
|
||||
const filePath = path.join(themePath, relativeSource);
|
||||
const fileName = fileToPath(relativeSource);
|
||||
|
||||
const aliasName = posixPath(
|
||||
normalizeUrl(['@theme', fileName]).replace(/\/$/, ''),
|
||||
);
|
||||
aliases[aliasName] = filePath;
|
||||
|
||||
if (addOriginalAlias) {
|
||||
// For swizzled components to access the original.
|
||||
const originalAliasName = posixPath(
|
||||
normalizeUrl(['@theme-original', fileName]).replace(/\/$/, ''),
|
||||
);
|
||||
aliases[originalAliasName] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return sortAliases(aliases);
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {THEME_PATH} from '@docusaurus/utils';
|
||||
import {themeAlias, sortAliases} from './alias';
|
||||
import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types';
|
||||
|
||||
const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback');
|
||||
|
||||
export async function loadThemeAliases(
|
||||
themePaths: string[],
|
||||
userThemePaths: string[],
|
||||
): Promise<ThemeAliases> {
|
||||
const aliases: ThemeAliases = {};
|
||||
|
||||
for (const themePath of themePaths) {
|
||||
const themeAliases = await themeAlias(themePath, true);
|
||||
Object.entries(themeAliases).forEach(([aliasKey, alias]) => {
|
||||
// If this alias shadows a previous one, use @theme-init to preserve the
|
||||
// initial one. @theme-init is only applied once: to the initial theme
|
||||
// that provided this component
|
||||
if (aliasKey in aliases) {
|
||||
const componentName = aliasKey.substring(aliasKey.indexOf('/') + 1);
|
||||
const initAlias = `@theme-init/${componentName}`;
|
||||
if (!(initAlias in aliases)) {
|
||||
aliases[initAlias] = aliases[aliasKey]!;
|
||||
}
|
||||
}
|
||||
aliases[aliasKey] = alias;
|
||||
});
|
||||
}
|
||||
|
||||
for (const themePath of userThemePaths) {
|
||||
const userThemeAliases = await themeAlias(themePath, false);
|
||||
Object.assign(aliases, userThemeAliases);
|
||||
}
|
||||
|
||||
return sortAliases(aliases);
|
||||
}
|
||||
|
||||
export function loadPluginsThemeAliases({
|
||||
siteDir,
|
||||
plugins,
|
||||
}: {
|
||||
siteDir: string;
|
||||
plugins: LoadedPlugin[];
|
||||
}): Promise<ThemeAliases> {
|
||||
const pluginThemes: string[] = plugins
|
||||
.map(
|
||||
(plugin) =>
|
||||
plugin.getThemePath && path.resolve(plugin.path, plugin.getThemePath()),
|
||||
)
|
||||
.filter((x): x is string => Boolean(x));
|
||||
const userTheme = path.resolve(siteDir, THEME_PATH);
|
||||
return loadThemeAliases([ThemeFallbackDir, ...pluginThemes], [userTheme]);
|
||||
}
|
|
@ -144,13 +144,10 @@ Maybe you should remove them? ${unknownKeys}`;
|
|||
}
|
||||
|
||||
// should we make this configurable?
|
||||
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 {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = `
|
||||
{
|
||||
"@docusaurus/BrowserOnly": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/BrowserOnly.tsx",
|
||||
"@docusaurus/ComponentCreator": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ComponentCreator.tsx",
|
||||
"@docusaurus/ErrorBoundary": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ErrorBoundary.tsx",
|
||||
"@docusaurus/ExecutionEnvironment": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/ExecutionEnvironment.ts",
|
||||
"@docusaurus/Head": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Head.tsx",
|
||||
"@docusaurus/Interpolate": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Interpolate.tsx",
|
||||
"@docusaurus/Link": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Link.tsx",
|
||||
"@docusaurus/Noop": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Noop.ts",
|
||||
"@docusaurus/Translate": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/Translate.tsx",
|
||||
"@docusaurus/constants": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/constants.ts",
|
||||
"@docusaurus/isInternalUrl": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/isInternalUrl.ts",
|
||||
"@docusaurus/renderRoutes": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/renderRoutes.ts",
|
||||
"@docusaurus/router": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/router.ts",
|
||||
"@docusaurus/useBaseUrl": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useBaseUrl.ts",
|
||||
"@docusaurus/useDocusaurusContext": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useDocusaurusContext.ts",
|
||||
"@docusaurus/useGlobalData": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useGlobalData.ts",
|
||||
"@docusaurus/useIsBrowser": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useIsBrowser.ts",
|
||||
"@docusaurus/useRouteContext": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useRouteContext.tsx",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadThemeAliases next alias can override the previous alias 1`] = `
|
||||
[
|
||||
[
|
||||
"@theme-init/Layout",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Layout/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme-original/Error",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme-original/Footer",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/Layout",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/Loading",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme-original/Navbar",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/NavbarItem/NestedNavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/NavbarItem/SiblingNavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/NavbarItem/zzz",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/NavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js",
|
||||
],
|
||||
[
|
||||
"@theme-original/NotFound",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme-original/Root",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Root/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme-original/SiteMetadata",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme/Error",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme/Footer",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js",
|
||||
],
|
||||
[
|
||||
"@theme/Layout",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js",
|
||||
],
|
||||
[
|
||||
"@theme/Loading",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme/Navbar",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js",
|
||||
],
|
||||
[
|
||||
"@theme/NavbarItem/NestedNavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js",
|
||||
],
|
||||
[
|
||||
"@theme/NavbarItem/SiblingNavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js",
|
||||
],
|
||||
[
|
||||
"@theme/NavbarItem/zzz",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js",
|
||||
],
|
||||
[
|
||||
"@theme/NavbarItem",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js",
|
||||
],
|
||||
[
|
||||
"@theme/NotFound",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme/Root",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Root/index.tsx",
|
||||
],
|
||||
[
|
||||
"@theme/SiteMetadata",
|
||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -5,9 +5,14 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
149
packages/docusaurus/src/webpack/aliases/index.ts
Normal file
149
packages/docusaurus/src/webpack/aliases/index.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {
|
||||
THEME_PATH,
|
||||
fileToPath,
|
||||
posixPath,
|
||||
normalizeUrl,
|
||||
Globby,
|
||||
} from '@docusaurus/utils';
|
||||
import _ from 'lodash';
|
||||
import type {ThemeAliases, LoadedPlugin} from '@docusaurus/types';
|
||||
|
||||
const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback');
|
||||
|
||||
/**
|
||||
* Order of Webpack aliases is important because one alias can shadow another.
|
||||
* This ensures `@theme/NavbarItem` alias is after
|
||||
* `@theme/NavbarItem/LocaleDropdown`.
|
||||
*
|
||||
* @see https://github.com/facebook/docusaurus/pull/3922
|
||||
* @see https://github.com/facebook/docusaurus/issues/5382
|
||||
*/
|
||||
export function sortAliases(aliases: ThemeAliases): ThemeAliases {
|
||||
// Alphabetical order by default
|
||||
const entries = _.sortBy(Object.entries(aliases), ([alias]) => alias);
|
||||
// @theme/NavbarItem should be after @theme/NavbarItem/LocaleDropdown
|
||||
entries.sort(([alias1], [alias2]) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
alias1.includes(`${alias2}/`) ? -1 : alias2.includes(`${alias1}/`) ? 1 : 0,
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
export async function createAliasesForTheme(
|
||||
themePath: string,
|
||||
addOriginalAlias: boolean,
|
||||
): Promise<ThemeAliases> {
|
||||
if (!(await fs.pathExists(themePath))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const themeComponentFiles = await Globby(['**/*.{js,jsx,ts,tsx}'], {
|
||||
cwd: themePath,
|
||||
});
|
||||
|
||||
const aliases: ThemeAliases = {};
|
||||
|
||||
themeComponentFiles.forEach((relativeSource) => {
|
||||
const filePath = path.join(themePath, relativeSource);
|
||||
const fileName = fileToPath(relativeSource);
|
||||
|
||||
const aliasName = posixPath(
|
||||
normalizeUrl(['@theme', fileName]).replace(/\/$/, ''),
|
||||
);
|
||||
aliases[aliasName] = filePath;
|
||||
|
||||
if (addOriginalAlias) {
|
||||
// For swizzled components to access the original.
|
||||
const originalAliasName = posixPath(
|
||||
normalizeUrl(['@theme-original', fileName]).replace(/\/$/, ''),
|
||||
);
|
||||
aliases[originalAliasName] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return sortAliases(aliases);
|
||||
}
|
||||
|
||||
async function createThemeAliases(
|
||||
themePaths: string[],
|
||||
userThemePaths: string[],
|
||||
): Promise<ThemeAliases> {
|
||||
const aliases: ThemeAliases = {};
|
||||
|
||||
for (const themePath of themePaths) {
|
||||
const themeAliases = await createAliasesForTheme(themePath, true);
|
||||
Object.entries(themeAliases).forEach(([aliasKey, alias]) => {
|
||||
// If this alias shadows a previous one, use @theme-init to preserve the
|
||||
// initial one. @theme-init is only applied once: to the initial theme
|
||||
// that provided this component
|
||||
if (aliasKey in aliases) {
|
||||
const componentName = aliasKey.substring(aliasKey.indexOf('/') + 1);
|
||||
const initAlias = `@theme-init/${componentName}`;
|
||||
if (!(initAlias in aliases)) {
|
||||
aliases[initAlias] = aliases[aliasKey]!;
|
||||
}
|
||||
}
|
||||
aliases[aliasKey] = alias;
|
||||
});
|
||||
}
|
||||
|
||||
for (const themePath of userThemePaths) {
|
||||
const userThemeAliases = await createAliasesForTheme(themePath, false);
|
||||
Object.assign(aliases, userThemeAliases);
|
||||
}
|
||||
|
||||
return sortAliases(aliases);
|
||||
}
|
||||
|
||||
export function loadThemeAliases({
|
||||
siteDir,
|
||||
plugins,
|
||||
}: {
|
||||
siteDir: string;
|
||||
plugins: LoadedPlugin[];
|
||||
}): Promise<ThemeAliases> {
|
||||
const pluginThemes: string[] = plugins
|
||||
.map(
|
||||
(plugin) =>
|
||||
plugin.getThemePath && path.resolve(plugin.path, plugin.getThemePath()),
|
||||
)
|
||||
.filter((x): x is string => Boolean(x));
|
||||
const userTheme = path.resolve(siteDir, THEME_PATH);
|
||||
return createThemeAliases([ThemeFallbackDir, ...pluginThemes], [userTheme]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: a `@docusaurus` alias would also catch `@docusaurus/theme-common`, so
|
||||
* instead of naively aliasing this to `client/exports`, we use fine-grained
|
||||
* aliases instead.
|
||||
*/
|
||||
export async function loadDocusaurusAliases(): Promise<{
|
||||
[aliasName: string]: string;
|
||||
}> {
|
||||
const dirPath = path.resolve(__dirname, '../../client/exports');
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
const aliases: {[key: string]: string} = {};
|
||||
|
||||
(await fs.readdir(dirPath))
|
||||
.filter((fileName) => extensions.includes(path.extname(fileName)))
|
||||
.forEach((fileName) => {
|
||||
const fileNameWithoutExtension = path.basename(
|
||||
fileName,
|
||||
path.extname(fileName),
|
||||
);
|
||||
const aliasName = `@docusaurus/${fileNameWithoutExtension}`;
|
||||
aliases[aliasName] = path.resolve(dirPath, fileName);
|
||||
});
|
||||
|
||||
return aliases;
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
getCustomBabelConfigFilePath,
|
||||
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
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Add table
Reference in a new issue