feat(v2): Collect plugin versions to allow them to be inspected in debug plugin (#3050)

This commit is contained in:
Sam Zhou 2020-07-13 09:46:48 -04:00 committed by GitHub
parent a3849860ae
commit 3ebe245b55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 193 additions and 13 deletions

View file

@ -37,6 +37,29 @@ declare module '@generated/routesChunkNames' {
export default 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<string, PluginVersionInformation>;
};
const siteMetadata: DocusaurusSiteMetadata;
export default siteMetadata;
}
declare module '@theme/*'; declare module '@theme/*';
declare module '@theme-original/*'; declare module '@theme-original/*';

View file

@ -10,6 +10,7 @@ import Layout from '@theme/Layout';
import registry from '@generated/registry'; import registry from '@generated/registry';
import routes from '@generated/routes'; import routes from '@generated/routes';
import siteMetadata from '@generated/site-metadata';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -17,7 +18,28 @@ function Debug() {
return ( return (
<Layout permalink="__docusaurus/debug" title="Debug"> <Layout permalink="__docusaurus/debug" title="Debug">
<main className={styles.Container}> <main className={styles.Container}>
<section> <section className={styles.Section}>
<h2>Site Metadata</h2>
<div>Docusaurus Version: {siteMetadata.docusaurusVersion}</div>
<div>
Site Version: {siteMetadata.siteVersion || 'No version specified'}
</div>
<h3>Plugins and themes:</h3>
<ul>
{Object.entries(siteMetadata.pluginVersions).map(
([name, versionInformation]) => (
<li key={name}>
<div>Name: {name}</div>
<div>Type: {versionInformation.type}</div>
{versionInformation.version && (
<div>Version: {versionInformation.version}</div>
)}
</li>
),
)}
</ul>
</section>
<section className={styles.Section}>
<h2>Registry</h2> <h2>Registry</h2>
<ul> <ul>
{Object.values(registry).map(([, aliasedPath, resolved]) => ( {Object.values(registry).map(([, aliasedPath, resolved]) => (
@ -28,7 +50,7 @@ function Debug() {
))} ))}
</ul> </ul>
</section> </section>
<section> <section className={styles.Section}>
<h2>Routes</h2> <h2>Routes</h2>
<ul> <ul>
{routes.map(({path, exact}) => ( {routes.map(({path, exact}) => (

View file

@ -7,5 +7,11 @@
.Container { .Container {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 1em; margin: 1em;
} }
.Section {
width: 500px;
}

View file

@ -6,7 +6,8 @@
*/ */
import {generate} from '@docusaurus/utils'; import {generate} from '@docusaurus/utils';
import path from 'path'; import {DocusaurusSiteMetadata} from '@generated/site-metadata';
import path, {join} from 'path';
import { import {
BUILD_DIR_NAME, BUILD_DIR_NAME,
CONFIG_FILE_NAME, CONFIG_FILE_NAME,
@ -26,6 +27,7 @@ import {
Props, Props,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {loadHtmlTags} from './html-tags'; import {loadHtmlTags} from './html-tags';
import {getPackageJsonVersion} from './versions';
export function loadContext( export function loadContext(
siteDir: string, siteDir: string,
@ -96,6 +98,7 @@ export async function load(
const {stylesheets = [], scripts = []} = siteConfig; const {stylesheets = [], scripts = []} = siteConfig;
plugins.push({ plugins.push({
name: 'docusaurus-bootstrap-plugin', name: 'docusaurus-bootstrap-plugin',
version: {type: 'synthetic'},
configureWebpack: () => ({ configureWebpack: () => ({
resolve: { resolve: {
alias, alias,
@ -178,12 +181,32 @@ ${Object.keys(registry)
const genRoutes = generate(generatedFilesDir, 'routes.js', routesConfig); 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([ await Promise.all([
genClientModules, genClientModules,
genSiteConfig, genSiteConfig,
genRegistry, genRegistry,
genRoutesChunkNames, genRoutesChunkNames,
genRoutes, genRoutes,
genSiteMetadata,
]); ]);
const props: Props = { const props: Props = {

View file

@ -10,12 +10,11 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { import {
LoadContext, LoadContext,
Plugin,
PluginConfig, PluginConfig,
PluginContentLoadedActions, PluginContentLoadedActions,
RouteConfig, RouteConfig,
} from '@docusaurus/types'; } from '@docusaurus/types';
import initPlugins from './init'; import initPlugins, {PluginWithVersionInformation} from './init';
export function sortConfig(routeConfigs: RouteConfig[]): void { export function sortConfig(routeConfigs: RouteConfig[]): void {
// Sort the route config. This ensures that route with nested // Sort the route config. This ensures that route with nested
@ -53,11 +52,14 @@ export async function loadPlugins({
pluginConfigs: PluginConfig[]; pluginConfigs: PluginConfig[];
context: LoadContext; context: LoadContext;
}): Promise<{ }): Promise<{
plugins: Plugin<unknown>[]; plugins: PluginWithVersionInformation[];
pluginsRouteConfigs: RouteConfig[]; pluginsRouteConfigs: RouteConfig[];
}> { }> {
// 1. Plugin Lifecycle - Initialization/Constructor. // 1. Plugin Lifecycle - Initialization/Constructor.
const plugins: Plugin<unknown>[] = initPlugins({pluginConfigs, context}); const plugins: PluginWithVersionInformation[] = initPlugins({
pluginConfigs,
context,
});
// 2. Plugin Lifecycle - loadContent. // 2. Plugin Lifecycle - loadContent.
// Currently plugins run lifecycle methods in parallel and are not order-dependent. // Currently plugins run lifecycle methods in parallel and are not order-dependent.

View file

@ -14,7 +14,9 @@ import {
PluginConfig, PluginConfig,
ValidationSchema, ValidationSchema,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {PluginVersionInformation} from '@generated/site-metadata';
import {CONFIG_FILE_NAME} from '../../constants'; import {CONFIG_FILE_NAME} from '../../constants';
import {getPluginVersion} from '../versions';
function validate<T>(schema: ValidationSchema<T>, options: Partial<T>) { function validate<T>(schema: ValidationSchema<T>, options: Partial<T>) {
const {error, value} = schema.validate(options, { const {error, value} = schema.validate(options, {
@ -37,13 +39,17 @@ function validateAndStrip<T>(schema: ValidationSchema<T>, options: Partial<T>) {
return value; return value;
} }
export type PluginWithVersionInformation = Plugin<unknown> & {
readonly version: PluginVersionInformation;
};
export default function initPlugins({ export default function initPlugins({
pluginConfigs, pluginConfigs,
context, context,
}: { }: {
pluginConfigs: PluginConfig[]; pluginConfigs: PluginConfig[];
context: LoadContext; context: LoadContext;
}): Plugin<unknown>[] { }): PluginWithVersionInformation[] {
// We need to resolve plugins from the perspective of the siteDir, since the siteDir's package.json // We need to resolve plugins from the perspective of the siteDir, since the siteDir's package.json
// declares the dependency on these plugins. // declares the dependency on these plugins.
// We need to fallback to createRequireFromPath since createRequire is only available in node v12. // 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 createRequire = Module.createRequire || Module.createRequireFromPath;
const pluginRequire = createRequire(join(context.siteDir, CONFIG_FILE_NAME)); const pluginRequire = createRequire(join(context.siteDir, CONFIG_FILE_NAME));
const plugins: Plugin<unknown>[] = pluginConfigs const plugins: PluginWithVersionInformation[] = pluginConfigs
.map((pluginItem) => { .map((pluginItem) => {
let pluginModuleImport: string | undefined; let pluginModuleImport: string | undefined;
let pluginOptions = {}; let pluginOptions = {};
@ -72,9 +78,9 @@ export default function initPlugins({
// The pluginModuleImport value is any valid // The pluginModuleImport value is any valid
// module identifier - npm package or locally-resolved path. // module identifier - npm package or locally-resolved path.
const pluginModule: any = importFresh( const pluginPath = pluginRequire.resolve(pluginModuleImport);
pluginRequire.resolve(pluginModuleImport), const pluginModule: any = importFresh(pluginPath);
); const pluginVersion = getPluginVersion(pluginPath, context.siteDir);
const plugin = pluginModule.default || pluginModule; const plugin = pluginModule.default || pluginModule;
@ -106,7 +112,7 @@ export default function initPlugins({
...normalizedThemeConfig, ...normalizedThemeConfig,
}; };
} }
return plugin(context, pluginOptions); return {...plugin(context, pluginOptions), version: pluginVersion};
}) })
.filter(Boolean); .filter(Boolean);
return plugins; return plugins;

View file

@ -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
/// <reference types="@docusaurus/module-type-aliases" />

View file

@ -0,0 +1,3 @@
{
"version": "random-version"
}

View file

@ -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'});
});
});

View file

@ -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'};
}