diff --git a/packages/create-docusaurus/templates/classic/docusaurus.config.js b/packages/create-docusaurus/templates/classic/docusaurus.config.js index ba5f8adcb7..406113b784 100644 --- a/packages/create-docusaurus/templates/classic/docusaurus.config.js +++ b/packages/create-docusaurus/templates/classic/docusaurus.config.js @@ -18,7 +18,7 @@ const config = { presets: [ [ - '@docusaurus/preset-classic', + 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { diff --git a/packages/create-docusaurus/templates/facebook/docusaurus.config.js b/packages/create-docusaurus/templates/facebook/docusaurus.config.js index 10ec80355f..f662a9889e 100644 --- a/packages/create-docusaurus/templates/facebook/docusaurus.config.js +++ b/packages/create-docusaurus/templates/facebook/docusaurus.config.js @@ -23,7 +23,7 @@ const config = { presets: [ [ - '@docusaurus/preset-classic', + 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index a9be044725..0bf54d2958 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -153,6 +153,14 @@ export interface Preset { themes?: PluginConfig[]; } +export type PresetModule = { + (context: LoadContext, presetOptions: T): Preset; +}; + +export type ImportedPresetModule = PresetModule & { + default?: PresetModule; +}; + export type PresetConfig = | [string, Record] | [string] diff --git a/packages/docusaurus/src/server/__tests__/moduleShorthand.test.ts b/packages/docusaurus/src/server/__tests__/moduleShorthand.test.ts new file mode 100644 index 0000000000..095fc6d53d --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/moduleShorthand.test.ts @@ -0,0 +1,90 @@ +/** + * 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 {getNamePatterns, resolveModuleName} from '../moduleShorthand'; + +describe('getNamePatterns', () => { + test('should resolve plain names', () => { + expect(getNamePatterns('awesome', 'plugin')).toEqual([ + 'awesome', + '@docusaurus/plugin-awesome', + 'docusaurus-plugin-awesome', + ]); + + expect(getNamePatterns('awesome', 'theme')).toEqual([ + 'awesome', + '@docusaurus/theme-awesome', + 'docusaurus-theme-awesome', + ]); + }); + + test('should expand bare scopes', () => { + expect(getNamePatterns('@joshcena', 'plugin')).toEqual([ + '@joshcena/docusaurus-plugin', + ]); + + expect(getNamePatterns('@joshcena', 'theme')).toEqual([ + '@joshcena/docusaurus-theme', + ]); + }); + + test('should expand scoped names', () => { + expect(getNamePatterns('@joshcena/awesome', 'plugin')).toEqual([ + '@joshcena/awesome', + '@joshcena/docusaurus-plugin-awesome', + ]); + + expect(getNamePatterns('@joshcena/awesome', 'theme')).toEqual([ + '@joshcena/awesome', + '@joshcena/docusaurus-theme-awesome', + ]); + }); + + test('should expand deep scoped paths', () => { + expect(getNamePatterns('@joshcena/awesome/web', 'plugin')).toEqual([ + '@joshcena/awesome/web', + '@joshcena/docusaurus-plugin-awesome/web', + ]); + + expect(getNamePatterns('@joshcena/awesome/web', 'theme')).toEqual([ + '@joshcena/awesome/web', + '@joshcena/docusaurus-theme-awesome/web', + ]); + }); +}); + +describe('resolveModuleName', () => { + test('should resolve longhand', () => { + expect( + resolveModuleName('@docusaurus/plugin-content-docs', require, 'plugin'), + ).toBeDefined(); + }); + + test('should resolve shorthand', () => { + expect(resolveModuleName('content-docs', require, 'plugin')).toBeDefined(); + }); + + test('should throw good error message for longhand', () => { + expect(() => + resolveModuleName('@docusaurus/plugin-content-doc', require, 'plugin'), + ).toThrowErrorMatchingInlineSnapshot(` + "Docusaurus was unable to resolve the \\"@docusaurus/plugin-content-doc\\" plugin. Make sure one of the following packages are installed: + - @docusaurus/plugin-content-doc + - @docusaurus/docusaurus-plugin-plugin-content-doc" + `); + }); + + test('should throw good error message for shorthand', () => { + expect(() => resolveModuleName('content-doc', require, 'plugin')) + .toThrowErrorMatchingInlineSnapshot(` + "Docusaurus was unable to resolve the \\"content-doc\\" plugin. Make sure one of the following packages are installed: + - content-doc + - @docusaurus/plugin-content-doc + - docusaurus-plugin-content-doc" + `); + }); +}); diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index f48c3b89d8..b6a4abf13a 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -39,6 +39,8 @@ import { import {mapValues} from 'lodash'; import {RuleSetRule} from 'webpack'; import admonitions from 'remark-admonitions'; +import {createRequire} from 'module'; +import {resolveModuleName} from './moduleShorthand'; export type LoadContextOptions = { customOutDir?: string; @@ -127,14 +129,44 @@ export async function loadContext( } export function loadPluginConfigs(context: LoadContext): PluginConfig[] { - const {plugins: presetPlugins, themes: presetThemes} = loadPresets(context); - const {siteConfig} = context; + let {plugins: presetPlugins, themes: presetThemes} = loadPresets(context); + const {siteConfig, siteConfigPath} = context; + const require = createRequire(siteConfigPath); + function normalizeShorthand( + pluginConfig: PluginConfig, + pluginType: 'plugin' | 'theme', + ): PluginConfig { + if (typeof pluginConfig === 'string') { + return resolveModuleName(pluginConfig, require, pluginType); + } else if ( + Array.isArray(pluginConfig) && + typeof pluginConfig[0] === 'string' + ) { + return [ + resolveModuleName(pluginConfig[0], require, pluginType), + pluginConfig[1] ?? {}, + ]; + } + return pluginConfig; + } + presetPlugins = presetPlugins.map((plugin) => + normalizeShorthand(plugin, 'plugin'), + ); + presetThemes = presetThemes.map((theme) => + normalizeShorthand(theme, 'theme'), + ); + const standalonePlugins = (siteConfig.plugins || []).map((plugin) => + normalizeShorthand(plugin, 'plugin'), + ); + const standaloneThemes = (siteConfig.themes || []).map((theme) => + normalizeShorthand(theme, 'theme'), + ); return [ ...presetPlugins, ...presetThemes, // Site config should be the highest priority. - ...(siteConfig.plugins || []), - ...(siteConfig.themes || []), + ...standalonePlugins, + ...standaloneThemes, ]; } diff --git a/packages/docusaurus/src/server/moduleShorthand.ts b/packages/docusaurus/src/server/moduleShorthand.ts new file mode 100644 index 0000000000..fce11f32b8 --- /dev/null +++ b/packages/docusaurus/src/server/moduleShorthand.ts @@ -0,0 +1,45 @@ +/** + * 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. + */ + +export function getNamePatterns( + moduleName: string, + moduleType: 'preset' | 'theme' | 'plugin', +): string[] { + if (moduleName.startsWith('@')) { + // Pure scope: `@scope` => `@scope/docusaurus-plugin` + if (!moduleName.includes('/')) { + return [`${moduleName}/docusaurus-${moduleType}`]; + } + const [scope, packageName] = moduleName.split(/\/(.*)/); + return [ + `${scope}/${packageName}`, + `${scope}/docusaurus-${moduleType}-${packageName}`, + ]; + } + return [ + moduleName, + `@docusaurus/${moduleType}-${moduleName}`, + `docusaurus-${moduleType}-${moduleName}`, + ]; +} + +export function resolveModuleName( + moduleName: string, + moduleRequire: NodeRequire, + moduleType: 'preset' | 'theme' | 'plugin', +): string { + const modulePatterns = getNamePatterns(moduleName, moduleType); + // eslint-disable-next-line no-restricted-syntax + for (const module of modulePatterns) { + try { + moduleRequire.resolve(module); + return module; + } catch (e) {} + } + throw new Error(`Docusaurus was unable to resolve the "${moduleName}" ${moduleType}. Make sure one of the following packages are installed: +${modulePatterns.map((module) => `- ${module}`).join('\n')}`); +} diff --git a/packages/docusaurus/src/server/presets/index.ts b/packages/docusaurus/src/server/presets/index.ts index 4975333b2b..61f468b360 100644 --- a/packages/docusaurus/src/server/presets/index.ts +++ b/packages/docusaurus/src/server/presets/index.ts @@ -10,17 +10,18 @@ import importFresh from 'import-fresh'; import { LoadContext, PluginConfig, - Preset, PresetConfig, + ImportedPresetModule, } from '@docusaurus/types'; +import {resolveModuleName} from '../moduleShorthand'; export default function loadPresets(context: LoadContext): { plugins: PluginConfig[]; themes: PluginConfig[]; } { - // We need to resolve plugins from the perspective of the siteDir, since the siteDir's package.json - // declares the dependency on these plugins. - const pluginRequire = createRequire(context.siteConfigPath); + // We need to resolve presets from the perspective of the siteDir, since the siteDir's package.json + // declares the dependency on these presets. + const presetRequire = createRequire(context.siteConfigPath); const presets: PresetConfig[] = (context.siteConfig || {}).presets || []; const unflatPlugins: PluginConfig[][] = []; @@ -36,17 +37,16 @@ export default function loadPresets(context: LoadContext): { } else { throw new Error('Invalid presets format detected in config.'); } + const presetName = resolveModuleName( + presetModuleImport, + presetRequire, + 'preset', + ); - type PresetInitializeFunction = ( - context: LoadContext, - presetOptions: Record, - ) => Preset; - const presetModule = importFresh< - PresetInitializeFunction & { - default?: PresetInitializeFunction; - } - >(pluginRequire.resolve(presetModuleImport)); - const preset: Preset = (presetModule.default || presetModule)( + const presetModule = importFresh( + presetRequire.resolve(presetName), + ); + const preset = (presetModule.default ?? presetModule)( context, presetOptions, ); @@ -60,7 +60,7 @@ export default function loadPresets(context: LoadContext): { }); return { - plugins: ([] as PluginConfig[]).concat(...unflatPlugins).filter(Boolean), - themes: ([] as PluginConfig[]).concat(...unflatThemes).filter(Boolean), + plugins: unflatPlugins.flat().filter(Boolean), + themes: unflatThemes.flat().filter(Boolean), }; } diff --git a/website/_dogfooding/dogfooding.config.js b/website/_dogfooding/dogfooding.config.js index efd1eed139..56ae83b230 100644 --- a/website/_dogfooding/dogfooding.config.js +++ b/website/_dogfooding/dogfooding.config.js @@ -10,7 +10,7 @@ const fs = require('fs'); /** @type {import('@docusaurus/types').PluginConfig[]} */ const dogfoodingPluginInstances = [ [ - '@docusaurus/plugin-content-docs', + 'content-docs', // dogfood shorthand /** @type {import('@docusaurus/plugin-content-docs').Options} */ ({ id: 'docs-tests', @@ -24,7 +24,7 @@ const dogfoodingPluginInstances = [ ], [ - '@docusaurus/plugin-content-blog', + '@docusaurus/plugin-content-blog', // dogfood longhand /** @type {import('@docusaurus/plugin-content-blog').Options} */ ({ id: 'blog-tests', @@ -46,7 +46,7 @@ const dogfoodingPluginInstances = [ ], [ - '@docusaurus/plugin-content-pages', + require.resolve('@docusaurus/plugin-content-pages'), // dogfood longhand resolve /** @type {import('@docusaurus/plugin-content-pages').Options} */ ({ id: 'pages-tests', diff --git a/website/docs/configuration.md b/website/docs/configuration.md index 58e21652ce..5192e40264 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -48,6 +48,65 @@ module.exports = { }; ``` +:::tip + +Docusaurus supports **module shorthands**, allowing you to simplify the above configuration as: + +```js title="docusaurus.config.js" +module.exports = { + // ... + plugins: ['content-blog', 'content-pages'], + themes: ['classic'], +}; +``` + +
+ +How are shorthands resolved? + +When it sees a plugin / theme / preset name, it tries to load one of the following, in that order: + +- `{name}` +- `@docusaurus/{type}-{name}` +- `docusaurus-{type}-{name}`, + +where `type` is one of `'preset'`, `'theme'`, `'plugin'`, depending on which field the module name is declared in. The first module name that's successfully found is loaded. + +If the name is scoped (beginning with `@`), the name is first split into scope and package name by the first slash: + +``` +@scope +^----^ + scope (no name!) + +@scope/awesome +^----^ ^-----^ + scope name + +@scope/awesome/main +^----^ ^----------^ + scope name +``` + +If the name is not specified, `{scope}/docusaurus-{type}` is loaded. Otherwise, the following are attempted: + +- `{scope}/{name}` +- `{scope}/docusaurus-{type}-{name}` + +Below are some examples, for a plugin registered in the `plugins` field. Note that unlike [ESLint](https://eslint.org/docs/user-guide/configuring/plugins#configuring-plugins) or [Babel](https://babeljs.io/docs/en/options#name-normalization) where a consistent naming convention for plugins is mandated, Docusaurus permits greater naming freedom, so the resolutions are not certain, but follows the priority defined above. + +| Declaration | May be resolved as | +| --- | --- | +| `awesome` | `docusaurus-plugin-awesome` | +| `sitemap` | [`@docusaurus/plugin-sitemap`](./api/plugins/plugin-sitemap.md) | +| `@mycompany` | `@mycompany/docusaurus-plugin` (the only possible resolution!) | +| `@mycompany/awesome` | `@mycompany/docusaurus-plugin-awesome` | +| `@mycompany/awesome/web` | `@mycompany/docusaurus-plugin-awesome/web` | + +
+ +::: + They can also be loaded from local directories: ```js title="docusaurus.config.js" @@ -66,7 +125,7 @@ module.exports = { // ... plugins: [ [ - '@docusaurus/plugin-content-blog', + 'content-blog', { path: 'blog', routeBasePath: 'blog', @@ -74,7 +133,7 @@ module.exports = { // ... }, ], - '@docusaurus/plugin-content-pages', + 'content-pages', ], }; ``` @@ -100,6 +159,12 @@ module.exports = { }; ``` +:::tip + +The `presets: [['classic', {...}]]` shorthand works as well. + +::: + For further help configuring themes, plugins, and presets, see [Using Themes](using-themes.md), [Using Plugins](using-plugins.md), and [Using Presets](presets.md). ### Custom configurations {#custom-configurations} diff --git a/website/docusaurus.config-blog-only.js b/website/docusaurus.config-blog-only.js index 33bca3ed33..bc6dd390f9 100644 --- a/website/docusaurus.config-blog-only.js +++ b/website/docusaurus.config-blog-only.js @@ -15,11 +15,11 @@ module.exports = { onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', favicon: 'img/docusaurus.ico', - themes: ['@docusaurus/theme-live-codeblock'], + themes: ['live-codeblock'], plugins: [], presets: [ [ - '@docusaurus/preset-classic', + 'classic', { docs: false, pages: false, diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index dabe04de80..9710bd1e5b 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -119,11 +119,11 @@ const config = { path.join(__dirname, '_dogfooding/_asset-tests'), ], clientModules: [require.resolve('./_dogfooding/clientModuleExample.ts')], - themes: ['@docusaurus/theme-live-codeblock'], + themes: ['live-codeblock'], plugins: [ FeatureRequestsPlugin, [ - '@docusaurus/plugin-content-docs', + 'content-docs', /** @type {import('@docusaurus/plugin-content-docs').Options} */ ({ id: 'community', @@ -142,7 +142,7 @@ const config = { }), ], [ - '@docusaurus/plugin-client-redirects', + 'client-redirects', /** @type {import('@docusaurus/plugin-client-redirects').Options} */ ({ fromExtensions: ['html'], @@ -171,7 +171,7 @@ const config = { }), ], [ - '@docusaurus/plugin-ideal-image', + 'ideal-image', { quality: 70, max: 1030, // max resized image's size. @@ -180,7 +180,7 @@ const config = { }, ], [ - '@docusaurus/plugin-pwa', + 'pwa', { debug: isDeployPreview, offlineModeActivationStrategies: [ @@ -244,7 +244,7 @@ const config = { ], presets: [ [ - '@docusaurus/preset-classic', + 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ debug: true, // force debug plugin usage