diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index ae41e1d54c3e..306c41e37393 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -37,6 +37,29 @@ declare module '@generated/routesChunkNames' { export default routesChunkNames; } +declare module '@generated/site-metadata' { + /** + * - `type: 'package'`, plugin is in a different package. + * - `type: 'project'`, plugin is in the same docusaurus project. + * - `type: 'local'`, none of plugin's ancestor directory contains any package.json. + * - `type: 'synthetic'`, docusaurus generated internal plugin. + */ + export type PluginVersionInformation = + | {readonly type: 'package'; readonly version?: string} + | {readonly type: 'project'} + | {readonly type: 'local'} + | {readonly type: 'synthetic'}; + + export type DocusaurusSiteMetadata = { + readonly docusaurusVersion: string; + readonly siteVersion?: string; + readonly pluginVersions: Record; + }; + + const siteMetadata: DocusaurusSiteMetadata; + export default siteMetadata; +} + declare module '@theme/*'; declare module '@theme-original/*'; diff --git a/packages/docusaurus-plugin-debug/src/theme/Debug/index.js b/packages/docusaurus-plugin-debug/src/theme/Debug/index.js index 74446ae2e5f3..1dc44d973a53 100644 --- a/packages/docusaurus-plugin-debug/src/theme/Debug/index.js +++ b/packages/docusaurus-plugin-debug/src/theme/Debug/index.js @@ -10,6 +10,7 @@ import Layout from '@theme/Layout'; import registry from '@generated/registry'; import routes from '@generated/routes'; +import siteMetadata from '@generated/site-metadata'; import styles from './styles.module.css'; @@ -17,7 +18,28 @@ function Debug() { return (
-
+
+

Site Metadata

+
Docusaurus Version: {siteMetadata.docusaurusVersion}
+
+ Site Version: {siteMetadata.siteVersion || 'No version specified'} +
+

Plugins and themes:

+
    + {Object.entries(siteMetadata.pluginVersions).map( + ([name, versionInformation]) => ( +
  • +
    Name: {name}
    +
    Type: {versionInformation.type}
    + {versionInformation.version && ( +
    Version: {versionInformation.version}
    + )} +
  • + ), + )} +
+
+

Registry

    {Object.values(registry).map(([, aliasedPath, resolved]) => ( @@ -28,7 +50,7 @@ function Debug() { ))}
-
+

Routes

    {routes.map(({path, exact}) => ( diff --git a/packages/docusaurus-plugin-debug/src/theme/Debug/styles.module.css b/packages/docusaurus-plugin-debug/src/theme/Debug/styles.module.css index cb621a15abf8..8dee72f3c8ac 100644 --- a/packages/docusaurus-plugin-debug/src/theme/Debug/styles.module.css +++ b/packages/docusaurus-plugin-debug/src/theme/Debug/styles.module.css @@ -7,5 +7,11 @@ .Container { display: flex; + flex-wrap: wrap; + justify-content: center; margin: 1em; } + +.Section { + width: 500px; +} diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 8d89c1edc817..c1d729027354 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -6,7 +6,8 @@ */ import {generate} from '@docusaurus/utils'; -import path from 'path'; +import {DocusaurusSiteMetadata} from '@generated/site-metadata'; +import path, {join} from 'path'; import { BUILD_DIR_NAME, CONFIG_FILE_NAME, @@ -26,6 +27,7 @@ import { Props, } from '@docusaurus/types'; import {loadHtmlTags} from './html-tags'; +import {getPackageJsonVersion} from './versions'; export function loadContext( siteDir: string, @@ -96,6 +98,7 @@ export async function load( const {stylesheets = [], scripts = []} = siteConfig; plugins.push({ name: 'docusaurus-bootstrap-plugin', + version: {type: 'synthetic'}, configureWebpack: () => ({ resolve: { alias, @@ -178,12 +181,32 @@ ${Object.keys(registry) const genRoutes = generate(generatedFilesDir, 'routes.js', routesConfig); + // Version metadata. + const siteMetadata: DocusaurusSiteMetadata = { + docusaurusVersion: getPackageJsonVersion( + join(__dirname, '../../package.json'), + )!, + siteVersion: getPackageJsonVersion(join(siteDir, 'package.json')), + pluginVersions: {}, + }; + plugins + .filter(({version: {type}}) => type !== 'synthetic') + .forEach(({name, version}) => { + siteMetadata.pluginVersions[name] = version; + }); + const genSiteMetadata = generate( + generatedFilesDir, + 'site-metadata.json', + JSON.stringify(siteMetadata, null, 2), + ); + await Promise.all([ genClientModules, genSiteConfig, genRegistry, genRoutesChunkNames, genRoutes, + genSiteMetadata, ]); const props: Props = { diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 100d28d4cda6..3657b2be7409 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -10,12 +10,11 @@ import fs from 'fs-extra'; import path from 'path'; import { LoadContext, - Plugin, PluginConfig, PluginContentLoadedActions, RouteConfig, } from '@docusaurus/types'; -import initPlugins from './init'; +import initPlugins, {PluginWithVersionInformation} from './init'; export function sortConfig(routeConfigs: RouteConfig[]): void { // Sort the route config. This ensures that route with nested @@ -53,11 +52,14 @@ export async function loadPlugins({ pluginConfigs: PluginConfig[]; context: LoadContext; }): Promise<{ - plugins: Plugin[]; + plugins: PluginWithVersionInformation[]; pluginsRouteConfigs: RouteConfig[]; }> { // 1. Plugin Lifecycle - Initialization/Constructor. - const plugins: Plugin[] = initPlugins({pluginConfigs, context}); + const plugins: PluginWithVersionInformation[] = initPlugins({ + pluginConfigs, + context, + }); // 2. Plugin Lifecycle - loadContent. // Currently plugins run lifecycle methods in parallel and are not order-dependent. diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 11b0fee25524..93a8fcb67ae9 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -14,7 +14,9 @@ import { PluginConfig, ValidationSchema, } from '@docusaurus/types'; +import {PluginVersionInformation} from '@generated/site-metadata'; import {CONFIG_FILE_NAME} from '../../constants'; +import {getPluginVersion} from '../versions'; function validate(schema: ValidationSchema, options: Partial) { const {error, value} = schema.validate(options, { @@ -37,13 +39,17 @@ function validateAndStrip(schema: ValidationSchema, options: Partial) { return value; } +export type PluginWithVersionInformation = Plugin & { + readonly version: PluginVersionInformation; +}; + export default function initPlugins({ pluginConfigs, context, }: { pluginConfigs: PluginConfig[]; context: LoadContext; -}): Plugin[] { +}): PluginWithVersionInformation[] { // 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 fallback to createRequireFromPath since createRequire is only available in node v12. @@ -51,7 +57,7 @@ export default function initPlugins({ const createRequire = Module.createRequire || Module.createRequireFromPath; const pluginRequire = createRequire(join(context.siteDir, CONFIG_FILE_NAME)); - const plugins: Plugin[] = pluginConfigs + const plugins: PluginWithVersionInformation[] = pluginConfigs .map((pluginItem) => { let pluginModuleImport: string | undefined; let pluginOptions = {}; @@ -72,9 +78,9 @@ export default function initPlugins({ // The pluginModuleImport value is any valid // module identifier - npm package or locally-resolved path. - const pluginModule: any = importFresh( - pluginRequire.resolve(pluginModuleImport), - ); + const pluginPath = pluginRequire.resolve(pluginModuleImport); + const pluginModule: any = importFresh(pluginPath); + const pluginVersion = getPluginVersion(pluginPath, context.siteDir); const plugin = pluginModule.default || pluginModule; @@ -106,7 +112,7 @@ export default function initPlugins({ ...normalizedThemeConfig, }; } - return plugin(context, pluginOptions); + return {...plugin(context, pluginOptions), version: pluginVersion}; }) .filter(Boolean); return plugins; diff --git a/packages/docusaurus/src/server/types.d.ts b/packages/docusaurus/src/server/types.d.ts new file mode 100644 index 000000000000..37777ca928d7 --- /dev/null +++ b/packages/docusaurus/src/server/types.d.ts @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// triple slash is important to keep, +// see https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html +// eslint-disable-next-line +/// diff --git a/packages/docusaurus/src/server/versions/__fixtures__/dummy-plugin.js b/packages/docusaurus/src/server/versions/__fixtures__/dummy-plugin.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/docusaurus/src/server/versions/__fixtures__/package.json b/packages/docusaurus/src/server/versions/__fixtures__/package.json new file mode 100644 index 000000000000..531c8b2b61d9 --- /dev/null +++ b/packages/docusaurus/src/server/versions/__fixtures__/package.json @@ -0,0 +1,3 @@ +{ + "version": "random-version" +} diff --git a/packages/docusaurus/src/server/versions/__tests/index.test.ts b/packages/docusaurus/src/server/versions/__tests/index.test.ts new file mode 100644 index 000000000000..d2eb6370eb19 --- /dev/null +++ b/packages/docusaurus/src/server/versions/__tests/index.test.ts @@ -0,0 +1,35 @@ +/** + * 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 {getPluginVersion} from '..'; +import {join} from 'path'; + +describe('getPluginVersion', () => { + it('Can detect external packages plugins versions of correctly.', () => { + expect( + getPluginVersion( + join(__dirname, '..', '__fixtures__', 'dummy-plugin.js'), + // Make the plugin appear external. + join(__dirname, '..', '..', '..', '..', '..', '..', 'website'), + ), + ).toEqual({type: 'package', version: 'random-version'}); + }); + + it('Can detect project plugins versions correctly.', () => { + expect( + getPluginVersion( + join(__dirname, '..', '__fixtures__', 'dummy-plugin.js'), + // Make the plugin appear project local. + join(__dirname, '..', '__fixtures__'), + ), + ).toEqual({type: 'project'}); + }); + + it('Can detect local packages versions correctly.', () => { + expect(getPluginVersion('/', '/')).toEqual({type: 'local'}); + }); +}); diff --git a/packages/docusaurus/src/server/versions/index.ts b/packages/docusaurus/src/server/versions/index.ts new file mode 100644 index 000000000000..03114a171227 --- /dev/null +++ b/packages/docusaurus/src/server/versions/index.ts @@ -0,0 +1,49 @@ +/** + * 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 {PluginVersionInformation} from '@generated/site-metadata'; +import {existsSync, lstatSync} from 'fs-extra'; +import {dirname, join} from 'path'; + +export function getPackageJsonVersion( + packageJsonPath: string, +): string | undefined { + if (existsSync(packageJsonPath)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require + const {version} = require(packageJsonPath); + return typeof version === 'string' ? version : undefined; + } + return undefined; +} + +export function getPluginVersion( + pluginPath: string, + siteDir: string, +): PluginVersionInformation { + let potentialPluginPackageJsonDirectory = dirname(pluginPath); + while (potentialPluginPackageJsonDirectory !== '/') { + const packageJsonPath = join( + potentialPluginPackageJsonDirectory, + 'package.json', + ); + if (existsSync(packageJsonPath) && lstatSync(packageJsonPath).isFile()) { + if (potentialPluginPackageJsonDirectory === siteDir) { + // If the plugin belongs to the same docusaurus project, we classify it as local plugin. + return {type: 'project'}; + } + return { + type: 'package', + version: getPackageJsonVersion(packageJsonPath), + }; + } + potentialPluginPackageJsonDirectory = dirname( + potentialPluginPackageJsonDirectory, + ); + } + // In rare cases where a plugin is a path where no parent directory contains package.json, we can only classify it as local. + return {type: 'local'}; +}