refactor(v2): use correct plugin types (#4418)

This commit is contained in:
Armano 2021-03-15 19:08:44 +01:00 committed by GitHub
parent 1078341b22
commit abae86f283
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 69 additions and 80 deletions

View file

@ -21,7 +21,6 @@ import {
STATIC_DIR_NAME, STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants'; } from '@docusaurus/core/lib/constants';
import {ValidationError} from 'joi';
import {flatten, take, kebabCase} from 'lodash'; import {flatten, take, kebabCase} from 'lodash';
import { import {
@ -56,7 +55,7 @@ import {
export default function pluginContentBlog( export default function pluginContentBlog(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<BlogContent | null, typeof PluginOptionSchema> { ): Plugin<BlogContent | null> {
if (options.admonitions) { if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([ options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, options.admonitions], [admonitions, options.admonitions],
@ -561,10 +560,7 @@ export default function pluginContentBlog(
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult< }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options); const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions; return validatedOptions;
} }

View file

@ -42,7 +42,6 @@ import {PermalinkToSidebar} from '@docusaurus/plugin-content-docs-types';
import {RuleSetRule} from 'webpack'; import {RuleSetRule} from 'webpack';
import {cliDocsVersionCommand} from './cli'; import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {OptionsSchema} from './options';
import {flatten, keyBy, compact} from 'lodash'; import {flatten, keyBy, compact} from 'lodash';
import {toGlobalDataVersion} from './globalData'; import {toGlobalDataVersion} from './globalData';
import {toVersionMetadataProp} from './props'; import {toVersionMetadataProp} from './props';
@ -54,7 +53,7 @@ import {
export default function pluginContentDocs( export default function pluginContentDocs(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<LoadedContent, typeof OptionsSchema> { ): Plugin<LoadedContent> {
const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context; const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context;
const versionsMetadata = readVersionsMetadata({context, options}); const versionsMetadata = readVersionsMetadata({context, options});

View file

@ -13,7 +13,6 @@ import {
URISchema, URISchema,
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {OptionValidationContext, ValidationResult} from '@docusaurus/types'; import {OptionValidationContext, ValidationResult} from '@docusaurus/types';
import {ValidationError} from 'joi';
import chalk from 'chalk'; import chalk from 'chalk';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
@ -89,14 +88,10 @@ export const OptionsSchema = Joi.object({
versions: VersionsOptionsSchema, versions: VersionsOptionsSchema,
}); });
// TODO bad validation function types
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult< }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
PluginOptions,
ValidationError
> {
// TODO remove homePageId before end of 2020 // TODO remove homePageId before end of 2020
// "slug: /" is better because the home doc can be different across versions // "slug: /" is better because the home doc can be different across versions
if (options.homePageId) { if (options.homePageId) {
@ -118,8 +113,7 @@ export function validateOptions({
options.includeCurrentVersion = !options.excludeNextVersionDocs; options.includeCurrentVersion = !options.excludeNextVersionDocs;
} }
// @ts-expect-error: TODO bad OptionValidationContext, need refactor const normalizedOptions = validate(OptionsSchema, options);
const normalizedOptions: PluginOptions = validate(OptionsSchema, options);
if (normalizedOptions.admonitions) { if (normalizedOptions.admonitions) {
normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([ normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([
@ -127,6 +121,5 @@ export function validateOptions({
]); ]);
} }
// @ts-expect-error: TODO bad OptionValidationContext, need refactor
return normalizedOptions; return normalizedOptions;
} }

View file

@ -29,7 +29,6 @@ import {
import {Configuration, Loader} from 'webpack'; import {Configuration, Loader} from 'webpack';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema'; import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from 'joi';
import { import {
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
STATIC_DIR_NAME, STATIC_DIR_NAME,
@ -53,7 +52,7 @@ const isMarkdownSource = (source: string) =>
export default function pluginContentPages( export default function pluginContentPages(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<LoadedContent | null, typeof PluginOptionSchema> { ): Plugin<LoadedContent | null> {
if (options.admonitions) { if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([ options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, options.admonitions || {}], [admonitions, options.admonitions || {}],
@ -260,10 +259,7 @@ export default function pluginContentPages(
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult< }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options); const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions; return validatedOptions;
} }

View file

@ -17,7 +17,6 @@ import {
Plugin, Plugin,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {PluginOptionSchema} from './pluginOptionSchema'; import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from 'joi';
export default function pluginSitemap( export default function pluginSitemap(
_context: LoadContext, _context: LoadContext,
@ -48,10 +47,7 @@ export default function pluginSitemap(
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult< }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options); const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions; return validatedOptions;
} }

View file

@ -68,10 +68,14 @@ function getInfimaCSSFile(direction) {
}.css`; }.css`;
} }
type PluginOptions = {
customCss?: string;
};
export default function docusaurusThemeClassic( export default function docusaurusThemeClassic(
context, context: any, // TODO: LoadContext is missing some of properties
options, options: PluginOptions,
): Plugin<null, unknown> { ): Plugin<void> {
const { const {
siteConfig: {themeConfig}, siteConfig: {themeConfig},
i18n: {currentLocale, localeConfigs}, i18n: {currentLocale, localeConfigs},

View file

@ -17,6 +17,7 @@
"@types/webpack": "^4.41.0", "@types/webpack": "^4.41.0",
"commander": "^5.1.0", "commander": "^5.1.0",
"querystring": "0.2.0", "querystring": "0.2.0",
"webpack-merge": "^4.2.2" "webpack-merge": "^4.2.2",
"joi": "^17.4.0"
} }
} }

View file

@ -7,10 +7,11 @@
// ESLint doesn't understand types dependencies in d.ts // ESLint doesn't understand types dependencies in d.ts
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import {Loader, Configuration, Stats} from 'webpack'; import type {Loader, Configuration, Stats} from 'webpack';
import {Command} from 'commander'; import type {Command} from 'commander';
import {ParsedUrlQueryInput} from 'querystring'; import type {ParsedUrlQueryInput} from 'querystring';
import {MergeStrategy} from 'webpack-merge'; import type {MergeStrategy} from 'webpack-merge';
import type Joi from 'joi';
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw'; export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
@ -183,7 +184,7 @@ export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
export interface Props extends LoadContext, InjectedHtmlTags { export interface Props extends LoadContext, InjectedHtmlTags {
routes: RouteConfig[]; routes: RouteConfig[];
routesPaths: string[]; routesPaths: string[];
plugins: Plugin<any, unknown>[]; plugins: Plugin<any>[];
} }
/** /**
@ -210,11 +211,9 @@ export type AllContent = Record<
// TODO improve type (not exposed by postcss-loader) // TODO improve type (not exposed by postcss-loader)
export type PostCssOptions = Record<string, any> & {plugins: any[]}; export type PostCssOptions = Record<string, any> & {plugins: any[]};
export interface Plugin<T, U = unknown> { export interface Plugin<T> {
name: string; name: string;
loadContent?(): Promise<T>; loadContent?(): Promise<T>;
validateOptions?(): ValidationResult<U>;
validateThemeConfig?(): ValidationResult<any>;
contentLoaded?({ contentLoaded?({
content, content,
actions, actions,
@ -242,7 +241,6 @@ export interface Plugin<T, U = unknown> {
preBodyTags?: HtmlTags; preBodyTags?: HtmlTags;
postBodyTags?: HtmlTags; postBodyTags?: HtmlTags;
}; };
getSwizzleComponentList?(): string[];
// TODO before/afterDevServer implementation // TODO before/afterDevServer implementation
// translations // translations
@ -269,6 +267,17 @@ export interface Plugin<T, U = unknown> {
}): ThemeConfig; }): ThemeConfig;
} }
export type PluginModule = {
<T, X>(context: LoadContext, options: T): Plugin<X>;
validateOptions?<T>(data: OptionValidationContext<T>): T;
validateThemeConfig?<T>(data: ThemeConfigValidationContext<T>): T;
getSwizzleComponentList?(): string[];
};
export type ImportedPluginModule = PluginModule & {
default?: PluginModule;
};
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack']; export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>; export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>;
export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss']; export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
@ -346,36 +355,25 @@ interface HtmlTagObject {
innerHTML?: string; innerHTML?: string;
} }
export interface ValidationResult<T, E extends Error = Error> { export type ValidationResult<T> = T;
error?: E;
value: T;
}
export type Validate<T, E extends Error = Error> = ( export type ValidationSchema<T> = Joi.ObjectSchema<T>;
export type Validate<T> = (
validationSchema: ValidationSchema<T>, validationSchema: ValidationSchema<T>,
options: Partial<T>, options: Partial<T>,
) => ValidationResult<T, E>; ) => ValidationResult<T>;
export interface OptionValidationContext<T, E extends Error = Error> { export interface OptionValidationContext<T> {
validate: Validate<T, E>; validate: Validate<T>;
options: Partial<T>; options: Partial<T>;
} }
export interface ThemeConfigValidationContext<T, E extends Error = Error> { export interface ThemeConfigValidationContext<T> {
validate: Validate<T, E>; validate: Validate<T>;
themeConfig: Partial<T>; themeConfig: Partial<T>;
} }
// TODO we should use a Joi type here
export interface ValidationSchema<T> {
validate(
options: Partial<T>,
opt: Record<string, unknown>,
): ValidationResult<T>;
unknown(): ValidationSchema<T>;
append(data: any): ValidationSchema<T>;
}
export interface TOCItem { export interface TOCItem {
readonly value: string; readonly value: string;
readonly id: string; readonly id: string;

View file

@ -37,7 +37,7 @@ export const logValidationBugReportHint = (): void => {
export function normalizePluginOptions<T extends {id?: string}>( export function normalizePluginOptions<T extends {id?: string}>(
schema: Joi.ObjectSchema<T>, schema: Joi.ObjectSchema<T>,
options: unknown, options: Partial<T>,
): T { ): T {
// All plugins can be provided an "id" option (multi-instance support) // All plugins can be provided an "id" option (multi-instance support)
// we add schema validation automatically // we add schema validation automatically
@ -61,7 +61,7 @@ export function normalizePluginOptions<T extends {id?: string}>(
export function normalizeThemeConfig<T>( export function normalizeThemeConfig<T>(
schema: Joi.ObjectSchema<T>, schema: Joi.ObjectSchema<T>,
themeConfig: unknown, themeConfig: Partial<T>,
): T { ): T {
// A theme should only validate his "slice" of the full themeConfig, // A theme should only validate his "slice" of the full themeConfig,
// not the whole object, so we allow unknown attributes // not the whole object, so we allow unknown attributes

View file

@ -9,7 +9,7 @@ import chalk = require('chalk');
import fs from 'fs-extra'; import fs from 'fs-extra';
import importFresh from 'import-fresh'; import importFresh from 'import-fresh';
import path from 'path'; import path from 'path';
import {Plugin, LoadContext, PluginConfig} from '@docusaurus/types'; import {ImportedPluginModule, PluginConfig} from '@docusaurus/types';
import leven from 'leven'; import leven from 'leven';
import {partition} from 'lodash'; import {partition} from 'lodash';
import {THEME_PATH} from '../constants'; import {THEME_PATH} from '../constants';
@ -31,9 +31,8 @@ export function getPluginNames(plugins: PluginConfig[]): string[] {
if (packagePath === '.') { if (packagePath === '.') {
return pluginPath; return pluginPath;
} }
return (importFresh(path.join(packagePath, 'package.json')) as { return importFresh<{name: string}>(path.join(packagePath, 'package.json'))
name: string; .name;
}).name as string;
}); });
} }
@ -66,7 +65,7 @@ function readComponent(themePath: string) {
// load components from theme based on configurations // load components from theme based on configurations
function getComponentName( function getComponentName(
themePath: string, themePath: string,
plugin: any, plugin: ImportedPluginModule,
danger: boolean, danger: boolean,
): Array<string> { ): Array<string> {
// support both commonjs and ES style exports // support both commonjs and ES style exports
@ -82,7 +81,10 @@ function getComponentName(
return readComponent(themePath); return readComponent(themePath);
} }
function themeComponents(themePath: string, plugin: Plugin<unknown>): string { function themeComponents(
themePath: string,
plugin: ImportedPluginModule,
): string {
const components = colorCode(themePath, plugin); const components = colorCode(themePath, plugin);
if (components.length === 0) { if (components.length === 0) {
@ -103,7 +105,10 @@ function formattedThemeNames(themeNames: string[]): string {
return `Themes available for swizzle:\n${themeNames.join('\n')}`; return `Themes available for swizzle:\n${themeNames.join('\n')}`;
} }
function colorCode(themePath: string, plugin: any): Array<string> { function colorCode(
themePath: string,
plugin: ImportedPluginModule,
): Array<string> {
// support both commonjs and ES style exports // support both commonjs and ES style exports
const getSwizzleComponentList = const getSwizzleComponentList =
plugin.default?.getSwizzleComponentList ?? plugin.getSwizzleComponentList; plugin.default?.getSwizzleComponentList ?? plugin.getSwizzleComponentList;
@ -148,11 +153,9 @@ export default async function swizzle(
process.exit(1); process.exit(1);
} }
let pluginModule; let pluginModule: ImportedPluginModule;
try { try {
pluginModule = importFresh(themeName) as ( pluginModule = importFresh(themeName);
context: LoadContext,
) => Plugin<unknown>;
} catch { } catch {
let suggestion; let suggestion;
themeNames.forEach((name) => { themeNames.forEach((name) => {
@ -170,10 +173,7 @@ export default async function swizzle(
process.exit(1); process.exit(1);
} }
const plugin = pluginModule.default ?? pluginModule; let pluginOptions = {};
const validateOptions =
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
let pluginOptions;
const resolvedThemeName = require.resolve(themeName); const resolvedThemeName = require.resolve(themeName);
// find the plugin from list of plugin and get options if specified // find the plugin from list of plugin and get options if specified
pluginConfigs.forEach((pluginConfig) => { pluginConfigs.forEach((pluginConfig) => {
@ -188,6 +188,9 @@ export default async function swizzle(
} }
}); });
// support both commonjs and ES style exports
const validateOptions =
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
if (validateOptions) { if (validateOptions) {
pluginOptions = validateOptions({ pluginOptions = validateOptions({
validate: normalizePluginOptions, validate: normalizePluginOptions,
@ -195,6 +198,8 @@ export default async function swizzle(
}); });
} }
// support both commonjs and ES style exports
const plugin = pluginModule.default ?? pluginModule;
const pluginInstance = plugin(context, pluginOptions); const pluginInstance = plugin(context, pluginOptions);
const themePath = typescript const themePath = typescript
? pluginInstance.getTypeScriptThemePath?.() ? pluginInstance.getTypeScriptThemePath?.()

View file

@ -9,6 +9,7 @@ import Module from 'module';
import importFresh from 'import-fresh'; import importFresh from 'import-fresh';
import { import {
DocusaurusPluginVersionInformation, DocusaurusPluginVersionInformation,
ImportedPluginModule,
LoadContext, LoadContext,
Plugin, Plugin,
PluginConfig, PluginConfig,
@ -68,7 +69,7 @@ For more information, visit https://v2.docusaurus.io/docs/using-plugins.`);
// 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 pluginPath = pluginRequire.resolve(pluginModuleImport); const pluginPath = pluginRequire.resolve(pluginModuleImport);
const pluginModule: any = importFresh(pluginPath); const pluginModule: ImportedPluginModule = importFresh(pluginPath);
const pluginVersion = getPluginVersion(pluginPath, context.siteDir); const pluginVersion = getPluginVersion(pluginPath, context.siteDir);
const plugin = pluginModule.default || pluginModule; const plugin = pluginModule.default || pluginModule;
@ -114,7 +115,7 @@ For more information, visit https://v2.docusaurus.io/docs/using-plugins.`);
version: pluginVersion, version: pluginVersion,
}; };
}) })
.filter(Boolean); .filter(<T>(item: T): item is Exclude<T, null> => Boolean(item));
ensureUniquePluginInstanceIds(plugins); ensureUniquePluginInstanceIds(plugins);