From 5fb09a2946ca6afcafdb2e96c0b08ba90afc65fc Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Mon, 28 Mar 2022 21:49:37 +0800 Subject: [PATCH] refactor(core): reorganize files (#7042) * refactor(core): reorganize files * fix types --- .../src/index.d.ts | 4 +- .../src/translations.ts | 6 +- .../src/__tests__/docs.test.ts | 5 +- .../src/__tests__/index.test.ts | 28 +- .../src/translations.ts | 7 +- .../src/__tests__/index.test.ts | 4 +- packages/docusaurus-types/src/index.d.ts | 208 +++++++------ packages/docusaurus/package.json | 1 - packages/docusaurus/src/commands/build.ts | 6 +- .../docusaurus/src/commands/commandUtils.ts | 2 +- packages/docusaurus/src/commands/deploy.ts | 3 +- packages/docusaurus/src/commands/external.ts | 2 +- packages/docusaurus/src/commands/serve.ts | 2 +- packages/docusaurus/src/commands/start.ts | 3 +- .../docusaurus/src/commands/swizzle/common.ts | 2 +- .../src/commands/swizzle/context.ts | 11 +- .../src/commands/writeHeadingIds.ts | 2 +- .../src/commands/writeTranslations.ts | 3 +- .../__snapshots__/config.test.ts.snap | 273 +++++++++--------- .../duplicateRoutes.test.ts.snap | 10 - .../__snapshots__/routes.test.ts.snap | 9 + .../src/server/__tests__/config.test.ts | 94 +++--- .../server/__tests__/duplicateRoutes.test.ts | 53 ---- .../src/server/__tests__/routes.test.ts | 61 +++- .../src/server/__tests__/testUtils.ts | 4 +- .../docusaurus/src/{ => server}/choosePort.ts | 19 +- .../docusaurus/src/server/clientModules.ts | 6 +- packages/docusaurus/src/server/config.ts | 32 +- .../docusaurus/src/server/duplicateRoutes.ts | 51 ---- packages/docusaurus/src/server/htmlTags.ts | 10 +- packages/docusaurus/src/server/i18n.ts | 3 +- packages/docusaurus/src/server/index.ts | 112 ++++--- .../src/server/plugins/__tests__/init.test.ts | 4 +- .../__tests__/moduleShorthand.test.ts | 0 .../docusaurus/src/server/plugins/configs.ts | 92 +++++- .../docusaurus/src/server/plugins/index.ts | 88 +++--- .../docusaurus/src/server/plugins/init.ts | 101 +------ .../server/{ => plugins}/moduleShorthand.ts | 0 .../src/server/plugins/pluginIds.ts | 6 +- .../docusaurus/src/server/plugins/presets.ts | 18 +- packages/docusaurus/src/server/routes.ts | 155 ++++++---- .../docusaurus/src/server/siteMetadata.ts | 11 +- .../src/server/themes/__tests__/index.test.ts | 61 ---- .../docusaurus/src/server/themes/alias.ts | 62 ---- .../docusaurus/src/server/themes/index.ts | 61 ---- .../src/server/translations/translations.ts | 5 +- .../__tests__/__snapshots__/base.test.ts.snap | 23 -- .../src/webpack/__tests__/base.test.ts | 18 +- .../__fixtures__/theme-1/Footer/index.js | 0 .../__tests__/__fixtures__/theme-1/Layout.js | 0 .../__fixtures__/theme-2/Layout/index.js | 0 .../__tests__/__fixtures__/theme-2/Navbar.js | 0 .../NavbarItem/NestedNavbarItem/index.js | 0 .../theme-2/NavbarItem/SiblingNavbarItem.js | 0 .../__fixtures__/theme-2/NavbarItem/index.js | 0 .../__fixtures__/theme-2/NavbarItem/zzz.js | 0 .../__snapshots__/index.test.ts.snap | 129 +++++++++ .../aliases/__tests__/index.test.ts} | 64 ++-- .../docusaurus/src/webpack/aliases/index.ts | 149 ++++++++++ packages/docusaurus/src/webpack/base.ts | 32 +- website/docs/docusaurus-core.md | 4 +- 61 files changed, 1090 insertions(+), 1029 deletions(-) delete mode 100644 packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap delete mode 100644 packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts rename packages/docusaurus/src/{ => server}/choosePort.ts (88%) delete mode 100644 packages/docusaurus/src/server/duplicateRoutes.ts rename packages/docusaurus/src/server/{ => plugins}/__tests__/moduleShorthand.test.ts (100%) rename packages/docusaurus/src/server/{ => plugins}/moduleShorthand.ts (100%) delete mode 100644 packages/docusaurus/src/server/themes/__tests__/index.test.ts delete mode 100644 packages/docusaurus/src/server/themes/alias.ts delete mode 100644 packages/docusaurus/src/server/themes/index.ts rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-1/Footer/index.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-1/Layout.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/Layout/index.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/Navbar.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/NavbarItem/index.js (100%) rename packages/docusaurus/src/{server/themes => webpack/aliases}/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js (100%) create mode 100644 packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap rename packages/docusaurus/src/{server/themes/__tests__/alias.test.ts => webpack/aliases/__tests__/index.test.ts} (72%) create mode 100644 packages/docusaurus/src/webpack/aliases/index.ts diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 3e5d0bc0663b..c069c7cb7adb 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -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; } diff --git a/packages/docusaurus-plugin-content-blog/src/translations.ts b/packages/docusaurus-plugin-content-blog/src/translations.ts index 460e24a62480..28c233dd99c8 100644 --- a/packages/docusaurus-plugin-content-blog/src/translations.ts +++ b/packages/docusaurus-plugin-content-blog/src/translations.ts @@ -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 { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index b7aa5cfe178b..0623a345cc2f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -168,7 +168,7 @@ describe('simple site', () => { loadSiteOptions: {options: Partial} = {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 = { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 8093071f166e..926bcfb8f96f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -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({ diff --git a/packages/docusaurus-plugin-content-docs/src/translations.ts b/packages/docusaurus-plugin-content-docs/src/translations.ts index cc43d6b3f061..4289f3579adf 100644 --- a/packages/docusaurus-plugin-content-docs/src/translations.ts +++ b/packages/docusaurus-plugin-content-docs/src/translations.ts @@ -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( diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts index 7e3df9c7d129..89221c819bc8 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts @@ -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, diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 4b8ec2208704..0d0db0682136 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -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, - { - title: Required; - url: Required; - baseUrl: Required; - i18n?: DeepPartial; - } +}; + +/** + * 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, + '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 & {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 = { (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; 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 { +export type Plugin = { name: string; loadContent?: () => Promise; contentLoaded?: (args: { @@ -273,19 +282,56 @@ export interface Plugin { // TODO before/afterDevServer implementation // translations - getTranslationFiles?: (args: {content: Content}) => Promise; + getTranslationFiles?: (args: { + content: Content; + }) => Promise; 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 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 & { +export type InitializedPlugin = Plugin & { readonly options: Required; readonly version: PluginVersionInformation; /** @@ -294,8 +340,8 @@ export type InitializedPlugin = Plugin & { readonly path: string; }; -export type LoadedPlugin = InitializedPlugin & { - 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 = { - (context: LoadContext, options: Options): - | Plugin - | Promise>; + (context: LoadContext, options: unknown): Plugin | Promise; validateOptions?: (data: OptionValidationContext) => U; validateThemeConfig?: (data: ThemeConfigValidationContext) => T; @@ -328,11 +372,11 @@ export type ImportedPluginModule = PluginModule & { default?: PluginModule; }; -export type ConfigureWebpackFn = Plugin['configureWebpack']; +export type ConfigureWebpackFn = Plugin['configureWebpack']; export type ConfigureWebpackFnMergeStrategy = { [key: string]: CustomizeRuleString; }; -export type ConfigurePostCssFn = Plugin['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 = Joi.ObjectSchema; @@ -444,10 +486,10 @@ export type OptionValidationContext = { options: T; }; -export interface ThemeConfigValidationContext { +export type ThemeConfigValidationContext = { validate: Validate; themeConfig: Partial; -} +}; export type TOCItem = { readonly value: string; diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index e4776f9e4895..544e9a85e7b1 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -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", diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index ef8df767c6e7..252c797d6a09 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -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, diff --git a/packages/docusaurus/src/commands/commandUtils.ts b/packages/docusaurus/src/commands/commandUtils.ts index cfa5f7e920c1..d14d5b8977ad 100644 --- a/packages/docusaurus/src/commands/commandUtils.ts +++ b/packages/docusaurus/src/commands/commandUtils.ts @@ -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'; diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index ac7400f349d4..2de40addb16e 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -38,7 +38,8 @@ export async function deploy( siteDir: string, cliOptions: Partial = {}, ): Promise { - const {outDir, siteConfig, siteConfigPath} = await loadContext(siteDir, { + const {outDir, siteConfig, siteConfigPath} = await loadContext({ + siteDir, customConfigFilePath: cliOptions.config, customOutDir: cliOptions.outDir, }); diff --git a/packages/docusaurus/src/commands/external.ts b/packages/docusaurus/src/commands/external.ts index 52d06b6f336f..eb8c1b94f26b 100644 --- a/packages/docusaurus/src/commands/external.ts +++ b/packages/docusaurus/src/commands/external.ts @@ -13,7 +13,7 @@ export async function externalCommand( cli: CommanderStatic, siteDir: string, ): Promise { - const context = await loadContext(siteDir); + const context = await loadContext({siteDir}); const plugins = await initPlugins(context); // Plugin Lifecycle - extendCli. diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index 039e477422e1..c1ef7e9e6e00 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -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'; diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index 81f2314c91b9..39da01713433 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -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? diff --git a/packages/docusaurus/src/commands/swizzle/common.ts b/packages/docusaurus/src/commands/swizzle/common.ts index 1dfdb04cf3ae..2d399b60a731 100644 --- a/packages/docusaurus/src/commands/swizzle/common.ts +++ b/packages/docusaurus/src/commands/swizzle/common.ts @@ -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']; diff --git a/packages/docusaurus/src/commands/swizzle/context.ts b/packages/docusaurus/src/commands/swizzle/context.ts index d14bce6407ee..721bedca758a 100644 --- a/packages/docusaurus/src/commands/swizzle/context.ts +++ b/packages/docusaurus/src/commands/swizzle/context.ts @@ -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 { - 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, })), }; diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts index 1fa8ca558b46..83481a8f0708 100644 --- a/packages/docusaurus/src/commands/writeHeadingIds.ts +++ b/packages/docusaurus/src/commands/writeHeadingIds.ts @@ -35,7 +35,7 @@ async function transformMarkdownFile( * transformed */ async function getPathsToWatch(siteDir: string): Promise { - const context = await loadContext(siteDir); + const context = await loadContext({siteDir}); const plugins = await initPlugins(context); return plugins.flatMap((plugin) => plugin?.getPathsToWatch?.() ?? []); } diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts index 2c2ddd5b8742..1e8a7f13637e 100644 --- a/packages/docusaurus/src/commands/writeTranslations.ts +++ b/packages/docusaurus/src/commands/writeTranslations.ts @@ -76,7 +76,8 @@ export async function writeTranslations( siteDir: string, options: WriteTranslationsOptions & ConfigOptions & {locale?: string}, ): Promise { - const context = await loadContext(siteDir, { + const context = await loadContext({ + siteDir, customConfigFilePath: options.config, locale: options.locale, }); diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 01a7674fb5c0..914b41fe1cdf 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -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": "/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": "/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": "/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", ], - }, - "noIndex": false, - "onBrokenLinks": "throw", - "onBrokenMarkdownLinks": "warn", - "onDuplicateRoutes": "warn", - "organizationName": "endiliey", - "plugins": [ - [ - "@docusaurus/plugin-content-docs", - { - "path": "../docs", - }, + "presets": [], + "projectName": "hello", + "scripts": [], + "staticDirectories": [ + "static", ], - "@docusaurus/plugin-content-pages", - ], - "presets": [], - "projectName": "hello", - "scripts": [], - "staticDirectories": [ - "static", - ], - "stylesheets": [], - "tagline": "Hello World", - "themeConfig": {}, - "themes": [], - "title": "Hello", - "titleDelimiter": "|", - "url": "https://docusaurus.io", + "stylesheets": [], + "tagline": "Hello World", + "themeConfig": {}, + "themes": [], + "title": "Hello", + "titleDelimiter": "|", + "url": "https://docusaurus.io", + }, + "siteConfigPath": "/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js", } `; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap deleted file mode 100644 index 77e5891b69d6..000000000000 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap +++ /dev/null @@ -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." -`; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap index 31aa85debae3..7870bab42380 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap @@ -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": { diff --git a/packages/docusaurus/src/server/__tests__/config.test.ts b/packages/docusaurus/src/server/__tests__/config.test.ts index 71363931e68e..7c3fb39b800b 100644 --- a/packages/docusaurus/src/server/__tests__/config.test.ts +++ b/packages/docusaurus/src/server/__tests__/config.test.ts @@ -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 \\"/packages/docusaurus/src/server/__tests__/__fixtures__/nonExisting/docusaurus.config.js\\" not found."`, ); }); }); diff --git a/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts b/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts deleted file mode 100644 index 055e59cef7aa..000000000000 --- a/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/docusaurus/src/server/__tests__/routes.test.ts b/packages/docusaurus/src/server/__tests__/routes.test.ts index 662ab03f9727..acac972d0f19 100644 --- a/packages/docusaurus/src/server/__tests__/routes.test.ts +++ b/packages/docusaurus/src/server/__tests__/routes.test.ts @@ -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(); }); }); diff --git a/packages/docusaurus/src/server/__tests__/testUtils.ts b/packages/docusaurus/src/server/__tests__/testUtils.ts index 06f29dbfc030..1d42d255f9e8 100644 --- a/packages/docusaurus/src/server/__tests__/testUtils.ts +++ b/packages/docusaurus/src/server/__tests__/testUtils.ts @@ -17,9 +17,9 @@ export default async function loadSetup(name: string): Promise { switch (name) { case 'custom': - return load(customSite); + return load({siteDir: customSite}); case 'simple': default: - return load(simpleSite); + return load({siteDir: simpleSite}); } } diff --git a/packages/docusaurus/src/choosePort.ts b/packages/docusaurus/src/server/choosePort.ts similarity index 88% rename from packages/docusaurus/src/choosePort.ts rename to packages/docusaurus/src/server/choosePort.ts index 8fec0ff0b9c8..11f06ea0c9e2 100644 --- a/packages/docusaurus/src/choosePort.ts +++ b/packages/docusaurus/src/server/choosePort.ts @@ -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 { @@ -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) { diff --git a/packages/docusaurus/src/server/clientModules.ts b/packages/docusaurus/src/server/clientModules.ts index 697e4c25288a..5a2057fe611c 100644 --- a/packages/docusaurus/src/server/clientModules.ts +++ b/packages/docusaurus/src/server/clientModules.ts @@ -8,7 +8,11 @@ import path from 'path'; import type {LoadedPlugin} from '@docusaurus/types'; -export function loadClientModules(plugins: LoadedPlugin[]): 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)) ?? diff --git a/packages/docusaurus/src/server/config.ts b/packages/docusaurus/src/server/config.ts index 05b3ad2e8bcd..84b4975f1249 100644 --- a/packages/docusaurus/src/server/config.ts +++ b/packages/docusaurus/src/server/config.ts @@ -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 { - 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> { + 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 - | Promise> - | (() => Partial) - | (() => Promise>); + const importedConfig = importFresh(siteConfigPath); const loadedConfig = typeof importedConfig === 'function' ? await importedConfig() : await importedConfig; - return validateConfig(loadedConfig); + const siteConfig = validateConfig(loadedConfig); + return {siteConfig, siteConfigPath}; } diff --git a/packages/docusaurus/src/server/duplicateRoutes.ts b/packages/docusaurus/src/server/duplicateRoutes.ts deleted file mode 100644 index abe0aecfa7c5..000000000000 --- a/packages/docusaurus/src/server/duplicateRoutes.ts +++ /dev/null @@ -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(); - 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); - } -} diff --git a/packages/docusaurus/src/server/htmlTags.ts b/packages/docusaurus/src/server/htmlTags.ts index 28e04cec80d1..66c497d8f1ce 100644 --- a/packages/docusaurus/src/server/htmlTags.ts +++ b/packages/docusaurus/src/server/htmlTags.ts @@ -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 { const pluginHtmlTags = plugins.map( (plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {}, ); diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 42b363be7de3..e171b21f78aa 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -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, ): Promise { const {i18n: i18nConfig} = config; diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index fc4c3afbeac4..bfdfc4f3c6d8 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -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 { - 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 { - // 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 { + 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; } diff --git a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts index 3155775b5266..3ecf57b0d860 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts @@ -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 = {}) { 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}; diff --git a/packages/docusaurus/src/server/__tests__/moduleShorthand.test.ts b/packages/docusaurus/src/server/plugins/__tests__/moduleShorthand.test.ts similarity index 100% rename from packages/docusaurus/src/server/__tests__/moduleShorthand.test.ts rename to packages/docusaurus/src/server/plugins/__tests__/moduleShorthand.test.ts diff --git a/packages/docusaurus/src/server/plugins/configs.ts b/packages/docusaurus/src/server/plugins/configs.ts index 94ca87424d3c..2662ddc69b99 100644 --- a/packages/docusaurus/src/server/plugins/configs.ts +++ b/packages/docusaurus/src/server/plugins/configs.ts @@ -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 { + // plugins: ["./plugin"] + if (typeof pluginConfig === 'string') { + const pluginModuleImport = pluginConfig; + const pluginPath = pluginRequire.resolve(pluginModuleImport); + const pluginModule = importFresh(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(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 { +): Promise { 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, + ), + ), + ); } diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 08002e8db72c..7484ae966669 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -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, }; } diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 326d041e8da2..14c01f2ace20 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -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 { - const pluginRequire = createRequire(configPath); - // plugins: ['./plugin'] - if (typeof pluginConfig === 'string') { - const pluginModuleImport = pluginConfig; - const pluginPath = pluginRequire.resolve(pluginModuleImport); - const pluginModule = importFresh(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(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 { - 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 { - // 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); diff --git a/packages/docusaurus/src/server/moduleShorthand.ts b/packages/docusaurus/src/server/plugins/moduleShorthand.ts similarity index 100% rename from packages/docusaurus/src/server/moduleShorthand.ts rename to packages/docusaurus/src/server/plugins/moduleShorthand.ts diff --git a/packages/docusaurus/src/server/plugins/pluginIds.ts b/packages/docusaurus/src/server/plugins/pluginIds.ts index bd0635e19c80..86e3eb5d9f97 100644 --- a/packages/docusaurus/src/server/plugins/pluginIds.ts +++ b/packages/docusaurus/src/server/plugins/pluginIds.ts @@ -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 { diff --git a/packages/docusaurus/src/server/plugins/presets.ts b/packages/docusaurus/src/server/plugins/presets.ts index d920ea2e59bd..877150cc6703 100644 --- a/packages/docusaurus/src/server/plugins/presets.ts +++ b/packages/docusaurus/src/server/plugins/presets.ts @@ -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> { + // 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; diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index 3bf9f8b37a08..39affe45a31d 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -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(); + 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; -} diff --git a/packages/docusaurus/src/server/siteMetadata.ts b/packages/docusaurus/src/server/siteMetadata.ts index 996244a94102..199dbc034a6b 100644 --- a/packages/docusaurus/src/server/siteMetadata.ts +++ b/packages/docusaurus/src/server/siteMetadata.ts @@ -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 { - const siteMetadata: DocusaurusSiteMetadata = { +}): Promise { + const siteMetadata: SiteMetadata = { docusaurusVersion: (await getPackageJsonVersion( path.join(__dirname, '../../package.json'), ))!, diff --git a/packages/docusaurus/src/server/themes/__tests__/index.test.ts b/packages/docusaurus/src/server/themes/__tests__/index.test.ts deleted file mode 100644 index aa70330d661f..000000000000 --- a/packages/docusaurus/src/server/themes/__tests__/index.test.ts +++ /dev/null @@ -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({}); - }); -}); diff --git a/packages/docusaurus/src/server/themes/alias.ts b/packages/docusaurus/src/server/themes/alias.ts deleted file mode 100644 index c5ee69aa0545..000000000000 --- a/packages/docusaurus/src/server/themes/alias.ts +++ /dev/null @@ -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 { - 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); -} diff --git a/packages/docusaurus/src/server/themes/index.ts b/packages/docusaurus/src/server/themes/index.ts deleted file mode 100644 index dbb8ca34e196..000000000000 --- a/packages/docusaurus/src/server/themes/index.ts +++ /dev/null @@ -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 { - 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 { - 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]); -} diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index a989378a0ccb..ae89e0308e20 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -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 { diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap index cfad026638f5..6fdffc625968 100644 --- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap +++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap @@ -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", -} -`; diff --git a/packages/docusaurus/src/webpack/__tests__/base.test.ts b/packages/docusaurus/src/webpack/__tests__/base.test.ts index 55482b3c2ce3..d3aff2ab5456 100644 --- a/packages/docusaurus/src/webpack/__tests__/base.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/base.test.ts @@ -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: '', diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-1/Footer/index.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-1/Footer/index.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-1/Layout.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Layout.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-1/Layout.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Layout.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/Layout/index.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/Layout/index.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/Navbar.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/Navbar.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/index.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/index.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js diff --git a/packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js b/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js similarity index 100% rename from packages/docusaurus/src/server/themes/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js rename to packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js diff --git a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000000..38a22a306e14 --- /dev/null +++ b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = ` +{ + "@docusaurus/BrowserOnly": "/packages/docusaurus/src/client/exports/BrowserOnly.tsx", + "@docusaurus/ComponentCreator": "/packages/docusaurus/src/client/exports/ComponentCreator.tsx", + "@docusaurus/ErrorBoundary": "/packages/docusaurus/src/client/exports/ErrorBoundary.tsx", + "@docusaurus/ExecutionEnvironment": "/packages/docusaurus/src/client/exports/ExecutionEnvironment.ts", + "@docusaurus/Head": "/packages/docusaurus/src/client/exports/Head.tsx", + "@docusaurus/Interpolate": "/packages/docusaurus/src/client/exports/Interpolate.tsx", + "@docusaurus/Link": "/packages/docusaurus/src/client/exports/Link.tsx", + "@docusaurus/Noop": "/packages/docusaurus/src/client/exports/Noop.ts", + "@docusaurus/Translate": "/packages/docusaurus/src/client/exports/Translate.tsx", + "@docusaurus/constants": "/packages/docusaurus/src/client/exports/constants.ts", + "@docusaurus/isInternalUrl": "/packages/docusaurus/src/client/exports/isInternalUrl.ts", + "@docusaurus/renderRoutes": "/packages/docusaurus/src/client/exports/renderRoutes.ts", + "@docusaurus/router": "/packages/docusaurus/src/client/exports/router.ts", + "@docusaurus/useBaseUrl": "/packages/docusaurus/src/client/exports/useBaseUrl.ts", + "@docusaurus/useDocusaurusContext": "/packages/docusaurus/src/client/exports/useDocusaurusContext.ts", + "@docusaurus/useGlobalData": "/packages/docusaurus/src/client/exports/useGlobalData.ts", + "@docusaurus/useIsBrowser": "/packages/docusaurus/src/client/exports/useIsBrowser.ts", + "@docusaurus/useRouteContext": "/packages/docusaurus/src/client/exports/useRouteContext.tsx", +} +`; + +exports[`loadThemeAliases next alias can override the previous alias 1`] = ` +[ + [ + "@theme-init/Layout", + "/packages/docusaurus/src/client/theme-fallback/Layout/index.tsx", + ], + [ + "@theme-original/Error", + "/packages/docusaurus/src/client/theme-fallback/Error/index.tsx", + ], + [ + "@theme-original/Footer", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js", + ], + [ + "@theme-original/Layout", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js", + ], + [ + "@theme-original/Loading", + "/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx", + ], + [ + "@theme-original/Navbar", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js", + ], + [ + "@theme-original/NavbarItem/NestedNavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js", + ], + [ + "@theme-original/NavbarItem/SiblingNavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js", + ], + [ + "@theme-original/NavbarItem/zzz", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js", + ], + [ + "@theme-original/NavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js", + ], + [ + "@theme-original/NotFound", + "/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx", + ], + [ + "@theme-original/Root", + "/packages/docusaurus/src/client/theme-fallback/Root/index.tsx", + ], + [ + "@theme-original/SiteMetadata", + "/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx", + ], + [ + "@theme/Error", + "/packages/docusaurus/src/client/theme-fallback/Error/index.tsx", + ], + [ + "@theme/Footer", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-1/Footer/index.js", + ], + [ + "@theme/Layout", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Layout/index.js", + ], + [ + "@theme/Loading", + "/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx", + ], + [ + "@theme/Navbar", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/Navbar.js", + ], + [ + "@theme/NavbarItem/NestedNavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/NestedNavbarItem/index.js", + ], + [ + "@theme/NavbarItem/SiblingNavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/SiblingNavbarItem.js", + ], + [ + "@theme/NavbarItem/zzz", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/zzz.js", + ], + [ + "@theme/NavbarItem", + "/packages/docusaurus/src/webpack/aliases/__tests__/__fixtures__/theme-2/NavbarItem/index.js", + ], + [ + "@theme/NotFound", + "/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx", + ], + [ + "@theme/Root", + "/packages/docusaurus/src/client/theme-fallback/Root/index.tsx", + ], + [ + "@theme/SiteMetadata", + "/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx", + ], +] +`; diff --git a/packages/docusaurus/src/server/themes/__tests__/alias.test.ts b/packages/docusaurus/src/webpack/aliases/__tests__/index.test.ts similarity index 72% rename from packages/docusaurus/src/server/themes/__tests__/alias.test.ts rename to packages/docusaurus/src/webpack/aliases/__tests__/index.test.ts index 72ed3945152c..b2154a41f5db 100644 --- a/packages/docusaurus/src/server/themes/__tests__/alias.test.ts +++ b/packages/docusaurus/src/webpack/aliases/__tests__/index.test.ts @@ -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(); + }); +}); diff --git a/packages/docusaurus/src/webpack/aliases/index.ts b/packages/docusaurus/src/webpack/aliases/index.ts new file mode 100644 index 000000000000..4f7e67a34ec8 --- /dev/null +++ b/packages/docusaurus/src/webpack/aliases/index.ts @@ -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 { + 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 { + 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 { + 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; +} diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts index 8754e70c9d3b..2c59b7d6a873 100644 --- a/packages/docusaurus/src/webpack/base.ts +++ b/packages/docusaurus/src/webpack/base.ts @@ -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 diff --git a/website/docs/docusaurus-core.md b/website/docs/docusaurus-core.md index 00a2848715c9..fcd35040dd0d 100644 --- a/website/docs/docusaurus-core.md +++ b/website/docs/docusaurus-core.md @@ -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; @@ -361,7 +361,7 @@ interface I18n { interface DocusaurusContext { siteConfig: DocusaurusConfig; - siteMetadata: DocusaurusSiteMetadata; + siteMetadata: SiteMetadata; globalData: Record; i18n: I18n; codeTranslations: Record;