diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d737589ccd9ef..9f2318bf32d64 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -587,6 +587,7 @@ packages/kbn-peggy @elastic/kibana-operations packages/kbn-peggy-loader @elastic/kibana-operations packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing packages/kbn-picomatcher @elastic/kibana-operations +packages/kbn-plugin-check @elastic/appex-sharedux packages/kbn-plugin-generator @elastic/kibana-operations packages/kbn-plugin-helpers @elastic/kibana-operations examples/portable_dashboards_example @elastic/kibana-presentation diff --git a/package.json b/package.json index 8755f072d7b57..0e16be404c878 100644 --- a/package.json +++ b/package.json @@ -594,6 +594,7 @@ "@kbn/paertial-results-example-plugin": "link:examples/partial_results_example", "@kbn/painless-lab-plugin": "link:x-pack/plugins/painless_lab", "@kbn/panel-loader": "link:packages/kbn-panel-loader", + "@kbn/plugin-check": "link:packages/kbn-plugin-check", "@kbn/portable-dashboards-example": "link:examples/portable_dashboards_example", "@kbn/preboot-example-plugin": "link:examples/preboot_example", "@kbn/presentation-util-plugin": "link:src/plugins/presentation_util", @@ -1653,7 +1654,7 @@ "terser-webpack-plugin": "^4.2.3", "tough-cookie": "^4.1.3", "tree-kill": "^1.2.2", - "ts-morph": "^13.0.2", + "ts-morph": "^15.1.0", "tsd": "^0.20.0", "typescript": "4.7.4", "url-loader": "^2.2.0", diff --git a/packages/kbn-docs-utils/index.ts b/packages/kbn-docs-utils/index.ts index 2df2b1f807ba9..111234b0044c1 100644 --- a/packages/kbn-docs-utils/index.ts +++ b/packages/kbn-docs-utils/index.ts @@ -7,3 +7,5 @@ */ export { runBuildApiDocsCli } from './src'; + +export { findPlugins, findTeamPlugins } from './src/find_plugins'; diff --git a/packages/kbn-docs-utils/src/find_plugins.ts b/packages/kbn-docs-utils/src/find_plugins.ts index c77816ba3b6c8..8fbe47ad90d02 100644 --- a/packages/kbn-docs-utils/src/find_plugins.ts +++ b/packages/kbn-docs-utils/src/find_plugins.ts @@ -33,7 +33,7 @@ function toApiScope(pkg: Package): ApiScope { } function toPluginOrPackage(pkg: Package): PluginOrPackage { - return { + const result = { id: pkg.isPlugin() ? pkg.manifest.plugin.id : pkg.manifest.id, directory: Path.resolve(REPO_ROOT, pkg.normalizedRepoRelativeDir), manifestPath: Path.resolve(REPO_ROOT, pkg.normalizedRepoRelativeDir, 'kibana.jsonc'), @@ -50,6 +50,20 @@ function toPluginOrPackage(pkg: Package): PluginOrPackage { }, scope: toApiScope(pkg), }; + + if (pkg.isPlugin()) { + return { + ...result, + manifest: { + ...result.manifest, + requiredPlugins: pkg.manifest.plugin.requiredPlugins || [], + optionalPlugins: pkg.manifest.plugin.optionalPlugins || [], + requiredBundles: pkg.manifest.plugin.requiredBundles || [], + }, + }; + } + + return result; } export function findPlugins(pluginOrPackageFilter?: string[]): PluginOrPackage[] { @@ -78,6 +92,18 @@ export function findPlugins(pluginOrPackageFilter?: string[]): PluginOrPackage[] } } +export function findTeamPlugins(team: string): PluginOrPackage[] { + const packages = getPackages(REPO_ROOT); + const plugins = packages.filter( + getPluginPackagesFilter({ + examples: false, + testPlugins: false, + }) + ); + + return [...plugins.filter((p) => p.manifest.owner.includes(team)).map(toPluginOrPackage)]; +} + /** * Helper to find packages. */ diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index 40faadb8c41f1..1d91d78a7c734 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -14,6 +14,9 @@ export interface PluginOrPackage { description?: string; owner: { name: string; githubTeam?: string }; serviceFolders: readonly string[]; + requiredBundles?: readonly string[]; + requiredPlugins?: readonly string[]; + optionalPlugins?: readonly string[]; }; isPlugin: boolean; directory: string; diff --git a/packages/kbn-plugin-check/.eslintrc.json b/packages/kbn-plugin-check/.eslintrc.json new file mode 100644 index 0000000000000..2f2f707c490b9 --- /dev/null +++ b/packages/kbn-plugin-check/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "import/no-extraneous-dependencies": "off" + } +} diff --git a/packages/kbn-plugin-check/README.md b/packages/kbn-plugin-check/README.md new file mode 100644 index 0000000000000..94803493375f1 --- /dev/null +++ b/packages/kbn-plugin-check/README.md @@ -0,0 +1,17 @@ +# @kbn/plugin-check + +This package contains a CLI to detect inconsistencies between the manifest and Typescript types of a Kibana plugin. Future work will include automatically fixing these inconsistencies. + +## Usage + +To check a single plugin, run the following command from the root of the Kibana repo: + +```sh +node scripts/plugin_check --plugin pluginName +``` + +To check all plugins owned by a team, run the following: + +```sh +node scripts/plugin_check --team @elastic/team_name +``` diff --git a/packages/kbn-plugin-check/const.ts b/packages/kbn-plugin-check/const.ts new file mode 100644 index 0000000000000..974de652ce4bf --- /dev/null +++ b/packages/kbn-plugin-check/const.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** Type types of plugin classes within a single plugin. */ +export const PLUGIN_LAYERS = ['server', 'client'] as const; + +/** The lifecycles a plugin class implements. */ +export const PLUGIN_LIFECYCLES = ['setup', 'start'] as const; + +/** An enum representing the dependency requirements for a plugin. */ +export const PLUGIN_REQUIREMENTS = ['required', 'optional'] as const; + +/** An enum representing the manifest requirements for a plugin. */ +export const MANIFEST_REQUIREMENTS = ['required', 'optional', 'bundle'] as const; + +/** The state of a particular dependency as it relates to the plugin manifest. */ +export const MANIFEST_STATES = ['required', 'optional', 'bundle', 'missing'] as const; + +/** + * The state of a particular dependency as it relates to a plugin class. Includes states where the + * plugin is missing properties to determine that state. + */ +export const PLUGIN_STATES = ['required', 'optional', 'missing', 'no class', 'unknown'] as const; + +/** The state of the dependency for the entire plugin. */ +export const DEPENDENCY_STATES = ['required', 'optional', 'mismatch'] as const; + +/** An enum representing how the dependency status was derived from the plugin class. */ +export const SOURCE_OF_TYPE = ['implements', 'method', 'none'] as const; diff --git a/packages/kbn-plugin-check/dependencies/create_table.ts b/packages/kbn-plugin-check/dependencies/create_table.ts new file mode 100644 index 0000000000000..ed282d05858d7 --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/create_table.ts @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Table, { Table as TableType } from 'cli-table3'; + +import colors from 'colors/safe'; + +import { ToolingLog } from '@kbn/tooling-log'; + +import { PluginLayer, PluginLifecycle, PluginInfo, PluginStatuses, PluginState } from '../types'; +import { PLUGIN_LAYERS, PLUGIN_LIFECYCLES } from '../const'; +import { borders } from './table_borders'; + +// A lot of this logic is brute-force and ugly. It's a quick and dirty way to get the +// proof-of-concept working. +export const createTable = ( + pluginInfo: PluginInfo, + statuses: PluginStatuses, + _log: ToolingLog +): TableType => { + const table = new Table({ + colAligns: ['left', 'center', 'center', 'center', 'center', 'center', 'center'], + style: { + 'padding-left': 2, + 'padding-right': 2, + }, + chars: borders.table, + }); + + const noDependencies = Object.keys(statuses).length === 0; + const noServerPlugin = pluginInfo.classes.server === null; + const noClientPlugin = pluginInfo.classes.client === null; + const noPlugins = noServerPlugin && noClientPlugin; + + if (noDependencies || noPlugins) { + table.push([pluginInfo.name]); + + if (noDependencies) { + table.push(['Plugin has no dependencies.']); + } + + if (noPlugins) + table.push([ + 'Plugin has no client or server implementation.\nIt should be migrated to a package, or only be a requiredBundle.', + ]); + + return table; + } + + /** + * Build and format the header cell for the plugin lifecycle column. + */ + const getLifecycleColumnHeader = (layer: PluginLayer, lifecycle: PluginLifecycle) => + Object.entries(statuses).some( + ([_name, statusObj]) => statusObj[layer][lifecycle].source === 'none' + ) + ? colors.red(lifecycle.toUpperCase()) + : lifecycle.toUpperCase(); + + /** + * Build and format the header cell for the plugin layer column. + */ + const getLayerColumnHeader = (layer: PluginLayer) => { + if (!pluginInfo.classes[layer]) { + return [ + { + colSpan: 2, + content: 'NO CLASS', + chars: borders.subheader, + }, + ]; + } + + return PLUGIN_LIFECYCLES.map((lifecycle) => ({ + content: getLifecycleColumnHeader(layer, lifecycle), + chars: borders.subheader, + })); + }; + + /** + * True if the `PluginState` is one of the states that should be excluded from a + * mismatch check. + */ + const isExcludedState = (state: PluginState) => + state === 'no class' || state === 'unknown' || state === 'missing'; + + const entries = Object.entries(statuses); + let hasPass = false; + let hasFail = false; + let hasWarn = false; + + // Table Header + table.push([ + { + colSpan: 3, + content: pluginInfo.name, + chars: borders.header, + }, + { + colSpan: 2, + content: 'SERVER', + chars: borders.header, + }, + { + colSpan: 2, + content: 'PUBLIC', + chars: borders.header, + }, + ]); + + // Table Subheader + table.push([ + { + content: '', + chars: borders.subheader, + }, + { + content: 'DEPENDENCY', + chars: borders.subheader, + }, + { + content: 'MANIFEST', + chars: borders.subheader, + }, + ...getLayerColumnHeader('server'), + ...getLayerColumnHeader('client'), + ]); + + // Dependency Rows + entries + .sort(([nameA], [nameB]) => { + return nameA.localeCompare(nameB); + }) + .forEach(([name, statusObj], index) => { + const { manifestState /* server, client*/ } = statusObj; + const chars = index === entries.length - 1 ? borders.lastDependency : {}; + const states = PLUGIN_LAYERS.flatMap((layer) => + PLUGIN_LIFECYCLES.flatMap((lifecycle) => statusObj[layer][lifecycle].pluginState) + ); + + // TODO: Clean all of this brute-force stuff up. + const getLifecycleCellContent = (state: string) => { + if (state === 'no class' || (manifestState === 'bundle' && state === 'missing')) { + return ''; + } else if (manifestState === 'bundle' || (manifestState !== state && state !== 'missing')) { + return colors.red(state === 'missing' ? '' : state); + } + + return state === 'missing' ? '' : state; + }; + + const hasNoMismatch = () => + states.some((state) => state === manifestState) && + states.filter((state) => state !== manifestState).every(isExcludedState); + + const isValidBundle = () => manifestState === 'bundle' && states.every(isExcludedState); + + const getStateLabel = () => { + if (hasNoMismatch() || isValidBundle()) { + hasPass = true; + return '✅'; + } else if (!hasNoMismatch()) { + hasFail = true; + return '❌'; + } + + hasWarn = true; + return '❓'; + }; + + const getLifecycleColumns = () => { + if (noClientPlugin && noServerPlugin) { + return [{ colSpan: 4, content: '' }]; + } + + return PLUGIN_LAYERS.flatMap((layer) => { + if (!pluginInfo.classes[layer]) { + return { colSpan: 2, content: '', chars }; + } + + return PLUGIN_LIFECYCLES.flatMap((lifecycle) => ({ + content: getLifecycleCellContent(statusObj[layer][lifecycle].pluginState), + chars, + })); + }); + }; + + table.push({ + [getStateLabel()]: [ + { content: name, chars }, + { + content: + manifestState === 'missing' ? colors.red(manifestState.toUpperCase()) : manifestState, + chars, + }, + ...getLifecycleColumns(), + ], + }); + }); + + table.push([ + { + colSpan: 7, + content: `${hasWarn ? '❓ - dependency is entirely missing or unknown.\n' : ''}${ + hasFail ? '❌ - dependency differs from the manifest.\n' : '' + }${hasPass ? '✅ - dependency matches the manifest.' : ''}`, + chars: borders.footer, + }, + ]); + + return table; +}; diff --git a/packages/kbn-plugin-check/dependencies/display_dependency_check.ts b/packages/kbn-plugin-check/dependencies/display_dependency_check.ts new file mode 100644 index 0000000000000..3ff1c904cfda8 --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/display_dependency_check.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginOrPackage } from '@kbn/docs-utils/src/types'; +import { ToolingLog } from '@kbn/tooling-log'; +import { Project } from 'ts-morph'; +import { inspect } from 'util'; +import { createTable } from './create_table'; +import { getDependencySummary } from './get_dependency_summary'; +import { getPluginInfo } from './get_plugin_info'; + +/** + * Prepare and output information about a plugin's dependencies. + */ +export const displayDependencyCheck = ( + project: Project, + plugin: PluginOrPackage, + log: ToolingLog +) => { + log.info('Running plugin check on plugin:', plugin.id); + log.indent(4); + + const pluginInfo = getPluginInfo(project, plugin, log); + + if (!pluginInfo) { + log.error(`Cannot find dependencies for plugin ${plugin.id}`); + return; + } + + log.debug('Building dependency summary...'); + + const summary = getDependencySummary(pluginInfo, log); + + log.debug(inspect(summary, true, null, true)); + + const table = createTable(pluginInfo, summary, log); + + log.indent(-4); + log.info(table.toString()); +}; diff --git a/packages/kbn-plugin-check/dependencies/get_dependency_summary.ts b/packages/kbn-plugin-check/dependencies/get_dependency_summary.ts new file mode 100644 index 0000000000000..75b3bdbc2a55c --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/get_dependency_summary.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { PluginInfo, DependencyState, PluginStatuses } from '../types'; + +import { PLUGIN_LAYERS, PLUGIN_LIFECYCLES } from '../const'; + +/** + * Prepares a summary of the plugin's dependencies, based on its manifest and plugin classes. + */ +export const getDependencySummary = (pluginInfo: PluginInfo, log: ToolingLog): PluginStatuses => { + const { + dependencies: { all, manifest, plugin }, + } = pluginInfo; + + const manifestDependencyNames = manifest.all; + + log.debug('All manifest dependencies:', manifestDependencyNames); + + const pluginDependencyNames = plugin.all; + + log.debug('All plugin dependencies:', all); + + // Combine all dependencies, removing duplicates. + const dependencyNames = [ + ...new Set([...pluginDependencyNames, ...manifestDependencyNames]), + ]; + + log.debug('All dependencies:', dependencyNames); + + const plugins: PluginStatuses = {}; + + // For each dependency, add the manifest state to the summary. + dependencyNames.forEach((name) => { + plugins[name] = plugins[name] || { + manifestState: manifest.required.includes(name) + ? 'required' + : manifest.optional.includes(name) + ? 'optional' + : manifest.bundle.includes(name) + ? 'bundle' + : 'missing', + }; + + // For each plugin layer... + PLUGIN_LAYERS.map((layer) => { + // ..initialize the layer object if it doesn't exist. + plugins[name][layer] = plugins[name][layer] || {}; + + // For each plugin lifecycle... + PLUGIN_LIFECYCLES.map((lifecycle) => { + // ...initialize the lifecycle object if it doesn't exist. + plugins[name][layer][lifecycle] = plugins[name][layer][lifecycle] || {}; + + const pluginLifecycle = plugin[layer][lifecycle]; + const source = pluginLifecycle?.source || 'none'; + + if (pluginInfo.classes[layer] === null) { + // If the plugin class for the layer doesn't exist-- e.g. it doesn't have a `server` implementation, + // then set the state to `no class`. + plugins[name][layer][lifecycle] = { typeName: '', pluginState: 'no class', source }; + } else if (source === 'none') { + // If the plugin class for the layer does exist, but the plugin doesn't implement the lifecycle, + // then set the state to `unknown`. + plugins[name][layer][lifecycle] = { typeName: '', pluginState: 'unknown', source }; + } else { + // Set the state of the dependency and its type name. + const typeName = pluginLifecycle?.typeName || `${lifecycle}Type`; + const pluginState = pluginLifecycle?.required.includes(name) + ? 'required' + : pluginLifecycle?.optional.includes(name) + ? 'optional' + : 'missing'; + plugins[name][layer][lifecycle] = { typeName, pluginState, source }; + } + }); + }); + }); + + // Once the statuses of all of the plugins are constructed, determine the overall state of the dependency + // relative to the plugin. + // + // For each dependency... + Object.entries(plugins).forEach(([name, nextPlugin]) => { + const { manifestState, client, server } = nextPlugin; + const { setup, start } = client; + const { setup: serverSetup, start: serverStart } = server; + + // ...create an array of unique states for the dependency derived from the manifest and plugin classes. + const state = [ + ...new Set([ + manifestState, + setup.pluginState, + start.pluginState, + serverSetup.pluginState, + serverStart.pluginState, + ]), + ]; + + // If there is more than one state in the array, then the dependency is in a mismatched state, e.g. + // the manifest states it's `required` but the impl claims it's `optional`. + let status: DependencyState = 'mismatch'; + + if (state.length === 1) { + if (state.includes('required')) { + status = 'required'; + } else if (state.includes('optional')) { + status = 'optional'; + } + } + + // Set the status of the dependency. + plugins[name].status = status; + }); + + return plugins; +}; diff --git a/packages/kbn-plugin-check/dependencies/get_plugin_info.ts b/packages/kbn-plugin-check/dependencies/get_plugin_info.ts new file mode 100644 index 0000000000000..b959f073625fc --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/get_plugin_info.ts @@ -0,0 +1,293 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ClassDeclaration, MethodDeclaration, Project, SyntaxKind, TypeNode } from 'ts-morph'; + +import { PluginOrPackage } from '@kbn/docs-utils/src/types'; +import { ToolingLog } from '@kbn/tooling-log'; + +import { getPluginClasses } from '../lib/get_plugin_classes'; +import { PluginInfo, PluginLifecycle, PluginLayer, Lifecycle, Dependencies } from '../types'; + +/** + * Derive and return information about a plugin and its dependencies. + */ +export const getPluginInfo = ( + project: Project, + plugin: PluginOrPackage, + log: ToolingLog +): PluginInfo | null => { + const { manifest } = plugin; + + const optionalManifestPlugins = manifest.optionalPlugins || []; + const requiredManifestPlugins = manifest.requiredPlugins || []; + const requiredManifestBundles = manifest.requiredBundles || []; + + const { client, server } = getPluginClasses(project, plugin, log); + + const clientDependencies = getPluginDependencies(client, 'client', log); + const serverDependencies = getPluginDependencies(server, 'server', log); + + // Combine all plugin implementation dependencies, removing duplicates. + const allPluginDependencies = [ + ...new Set([...clientDependencies.all, ...serverDependencies.all]), + ]; + + // Combine all manifest dependencies, removing duplicates. + const allManifestDependencies = [ + ...new Set([ + ...requiredManifestPlugins, + ...optionalManifestPlugins, + ...requiredManifestBundles, + ]), + ]; + + return { + name: plugin.id, + project, + classes: { + client, + server, + }, + dependencies: { + all: [...new Set([...allManifestDependencies, ...allPluginDependencies])], + manifest: { + all: allManifestDependencies, + required: requiredManifestPlugins, + optional: optionalManifestPlugins, + bundle: requiredManifestBundles, + }, + plugin: { + all: allPluginDependencies, + client: clientDependencies, + server: serverDependencies, + }, + }, + }; +}; + +const getPluginDependencies = ( + pluginClass: ClassDeclaration | null, + pluginType: 'client' | 'server', + log: ToolingLog +): Lifecycle => { + // If the plugin class doesn't exist, return an empty object with `null` implementations. + if (!pluginClass) { + return { + all: [], + setup: null, + start: null, + }; + } + + // This is all very brute-force, but it's easier to see and understand what's going on, rather + // than relying on loops and placeholders. YMMV. + const { + source: setupSource, + typeName: setupType, + optional: optionalSetupDependencies, + required: requiredSetupDependencies, + } = getDependenciesFromLifecycleType(pluginClass, 'setup', pluginType, log); + + const { + source: startSource, + typeName: startType, + optional: optionalStartDependencies, + required: requiredStartDependencies, + } = getDependenciesFromLifecycleType(pluginClass, 'start', pluginType, log); + + return { + all: [ + ...new Set([ + ...requiredSetupDependencies, + ...optionalSetupDependencies, + ...requiredStartDependencies, + ...optionalStartDependencies, + ]), + ], + setup: { + all: [...new Set([...requiredSetupDependencies, ...optionalSetupDependencies])], + source: setupSource, + typeName: setupType, + required: requiredSetupDependencies, + optional: optionalSetupDependencies, + }, + start: { + all: [...new Set([...requiredStartDependencies, ...optionalStartDependencies])], + source: startSource, + typeName: startType, + required: requiredStartDependencies, + optional: optionalStartDependencies, + }, + }; +}; + +/** + * Given a lifecycle type, derive the dependencies for that lifecycle. + */ +const getDependenciesFromLifecycleType = ( + pluginClass: ClassDeclaration, + lifecycle: PluginLifecycle, + layer: PluginLayer, + log: ToolingLog +): Dependencies => { + const className = pluginClass.getName(); + log.debug(`${layer}/${className}/${lifecycle} discovering dependencies.`); + + const classImplements = pluginClass.getImplements(); + + if (!classImplements || classImplements.length === 0) { + log.warning(`${layer}/${className} plugin class does not extend the Core Plugin interface.`); + } else { + // This is safe, as we don't allow more than one class per file. + const typeArguments = classImplements[0].getTypeArguments(); + + // The `Plugin` generic has 4 type arguments, the 3rd of which is an interface of `setup` + // dependencies, and the fourth being an interface of `start` dependencies. + const type = typeArguments[lifecycle === 'setup' ? 2 : 3]; + + // If the type is defined, we can derive the dependencies directly from it. + if (type) { + const dependencies = getDependenciesFromNode(type, log); + + if (dependencies) { + return dependencies; + } + } else { + // ...and we can warn if the type is not defined. + log.warning( + `${layer}/${className}/${lifecycle} dependencies not defined on core interface generic.` + ); + } + } + + // If the type is not defined or otherwise unavailable, it's possible to derive the lifecycle + // dependencies directly from the instance method. + log.debug( + `${layer}/${className}/${lifecycle} falling back to instance method to derive dependencies.` + ); + + const methods = pluginClass.getInstanceMethods(); + + // Find the method on the class that matches the lifecycle name. + const method = methods.find((m) => m.getName() === (lifecycle === 'setup' ? 'setup' : 'start')); + + // As of now, a plugin cannot omit a lifecycle method, so throw an error. + if (!method) { + throw new Error( + `${layer}/${className}/${lifecycle} method does not exist; this should not be possible.` + ); + } + + // Given a method, derive the dependencies. + const dependencies = getDependenciesFromMethod(method, log); + + if (dependencies) { + return dependencies; + } + + log.warning( + `${layer}/${className}/${lifecycle} dependencies also not defined on lifecycle method.` + ); + + // At this point, there's no way to derive the dependencies, so return an empty object. + return { + all: [], + source: 'none', + typeName: null, + required: [], + optional: [], + }; +}; + +/** Derive dependencies from a `TypeNode`-- the lifecycle method itself. */ +const getDependenciesFromNode = ( + node: TypeNode | undefined, + _log: ToolingLog +): Dependencies | null => { + if (!node) { + return null; + } + + const typeName = node.getText(); + + // Get all of the dependencies and whether or not they're required. + const dependencies = node + .getType() + .getSymbol() + ?.getMembers() + .map((member) => { + return { name: member.getName(), isOptional: member.isOptional() }; + }); + + // Split the dependencies into required and optional. + const optional = + dependencies + ?.filter((dependency) => dependency.isOptional) + .map((dependency) => dependency.name) || []; + + const required = + dependencies + ?.filter((dependency) => !dependency.isOptional) + .map((dependency) => dependency.name) || []; + + return { + all: [...new Set([...required, ...optional])], + // Set the `source` to `implements`, as the dependencies were derived from the method + // implementation, rather than an explicit type. + source: 'implements', + typeName, + required, + optional, + }; +}; + +const getDependenciesFromMethod = ( + method: MethodDeclaration, + _log: ToolingLog +): Dependencies | null => { + if (!method) { + return null; + } + + const dependencyObj = method.getParameters()[1]; + + if (!dependencyObj) { + return null; + } + + const typeRef = dependencyObj.getDescendantsOfKind(SyntaxKind.TypeReference)[0]; + + if (!typeRef) { + return null; + } + + const symbol = typeRef.getType().getSymbol(); + + const dependencies = symbol?.getMembers().map((member) => { + return { name: member.getName(), isOptional: member.isOptional() }; + }); + + const optional = + dependencies + ?.filter((dependency) => dependency.isOptional) + .map((dependency) => dependency.name) || []; + + const required = + dependencies + ?.filter((dependency) => !dependency.isOptional) + .map((dependency) => dependency.name) || []; + + return { + all: [...new Set([...required, ...optional])], + source: 'method', + typeName: symbol?.getName() || null, + required, + optional, + }; +}; diff --git a/packages/kbn-plugin-check/dependencies/index.ts b/packages/kbn-plugin-check/dependencies/index.ts new file mode 100644 index 0000000000000..5a77c64c25bc4 --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Flags } from '@kbn/dev-cli-runner'; +import { findTeamPlugins } from '@kbn/docs-utils'; +import { ToolingLog } from '@kbn/tooling-log'; +import { Project } from 'ts-morph'; +import { getPlugin } from '../lib'; +import { displayDependencyCheck } from './display_dependency_check'; + +export const checkDependencies = (flags: Flags, log: ToolingLog) => { + const checkPlugin = (name: string) => { + const plugin = getPlugin(name, log); + + if (!plugin) { + log.error(`Cannot find plugin ${name}`); + return; + } + + const project = new Project({ + tsConfigFilePath: `${plugin.directory}/tsconfig.json`, + }); + + displayDependencyCheck(project, plugin, log); + }; + + const pluginOrTeam = typeof flags.dependencies === 'string' ? flags.dependencies : undefined; + + if (!pluginOrTeam) { + return; + } + + if (pluginOrTeam.startsWith('@elastic/')) { + const plugins = findTeamPlugins(pluginOrTeam); + + plugins.forEach((plugin) => { + checkPlugin(plugin.manifest.id); + }); + } else { + checkPlugin(pluginOrTeam); + } +}; diff --git a/packages/kbn-plugin-check/dependencies/table_borders.ts b/packages/kbn-plugin-check/dependencies/table_borders.ts new file mode 100644 index 0000000000000..523c86dcdf6fa --- /dev/null +++ b/packages/kbn-plugin-check/dependencies/table_borders.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CharName } from 'cli-table3'; + +type Borders = Record>>; + +/** + * A utility collection of table border settings for use with `cli-table3`. + */ +export const borders: Borders = { + table: { + 'bottom-left': '╚', + 'bottom-right': '╝', + 'left-mid': '╟', + 'right-mid': '╢', + 'top-left': '╔', + 'top-right': '╗', + left: '║', + right: '║', + }, + header: { + 'bottom-left': '╚', + 'bottom-mid': '╤', + 'bottom-right': '╝', + 'mid-mid': '╪', + 'top-mid': '╤', + bottom: '═', + mid: '═', + top: '═', + }, + subheader: { + 'left-mid': '╠', + 'mid-mid': '╪', + 'right-mid': '╣', + 'top-left': '╠', + 'top-mid': '╤', + 'top-right': '╣', + bottom: '═', + mid: '═', + top: '╤', + }, + lastDependency: { + 'bottom-left': '╚', + 'bottom-mid': '═', + 'bottom-right': '╝', + bottom: '═', + }, + footer: { + 'bottom-left': '╚', + 'bottom-mid': '╧', + 'bottom-right': '╝', + 'left-mid': '╠', + 'mid-mid': '═', + 'right-mid': '╣', + 'top-left': '╠', + 'top-mid': '╤', + 'top-right': '╣', + bottom: '═', + left: '║', + mid: '═', + middle: '═', + right: '║', + top: '═', + }, +}; diff --git a/packages/kbn-plugin-check/dependents.ts b/packages/kbn-plugin-check/dependents.ts new file mode 100644 index 0000000000000..b3b090c2eafd2 --- /dev/null +++ b/packages/kbn-plugin-check/dependents.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/tooling-log'; + +import { getAllPlugins } from './lib'; + +interface Dependents { + required: readonly string[]; + optional: readonly string[]; + bundles?: readonly string[]; +} + +export const findDependents = (plugin: string, log: ToolingLog): Dependents => { + log.info(`Finding dependents for ${plugin}`); + const plugins = getAllPlugins(log); + const required: string[] = []; + const optional: string[] = []; + const bundles: string[] = []; + + plugins.forEach((p) => { + const manifest = p.manifest; + + if (manifest.requiredPlugins?.includes(plugin)) { + required.push(manifest.id); + } + + if (manifest.optionalPlugins?.includes(plugin)) { + optional.push(manifest.id); + } + + if (manifest.requiredBundles?.includes(plugin)) { + bundles.push(manifest.id); + } + }); + + if (required.length === 0 && optional.length === 0 && bundles.length === 0) { + log.info(`No plugins depend on ${plugin}`); + } + + if (required.length > 0) { + log.info(`REQUIRED BY ${required.length}:\n${required.join('\n')}\n`); + } + + if (optional.length > 0) { + log.info(`OPTIONAL FOR ${optional.length}:\n${optional.join('\n')}\n`); + } + + if (bundles.length > 0) { + log.info(`BUNDLE FOR ${bundles.length}:\n${bundles.join('\n')}\n`); + } + + return { + required, + optional, + bundles, + }; +}; diff --git a/packages/kbn-plugin-check/index.ts b/packages/kbn-plugin-check/index.ts new file mode 100644 index 0000000000000..ffb38708bc5c0 --- /dev/null +++ b/packages/kbn-plugin-check/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { run } from '@kbn/dev-cli-runner'; +import { checkDependencies } from './dependencies'; +import { rankDependencies } from './rank'; +import { findDependents } from './dependents'; + +/** + * A CLI for checking the consistency of a plugin's declared and implicit dependencies. + */ +export const runPluginCheckCli = () => { + run( + async ({ log, flags }) => { + if ( + (flags.dependencies && flags.rank) || + (flags.dependencies && flags.dependents) || + (flags.rank && flags.dependents) + ) { + throw new Error('Only one of --dependencies, --rank, or --dependents may be specified.'); + } + + if (flags.dependencies) { + checkDependencies(flags, log); + } + + if (flags.rank) { + rankDependencies(log); + } + + if (flags.dependents && typeof flags.dependents === 'string') { + findDependents(flags.dependents, log); + } + }, + { + log: { + defaultLevel: 'info', + }, + flags: { + boolean: ['rank'], + string: ['dependencies', 'dependents'], + help: ` + --rank Display plugins as a ranked list of usage. + --dependents [plugin] Display plugins that depend on a given plugin. + --dependencies [plugin] Check plugin dependencies for a single plugin. + --dependencies [team] Check dependencies for all plugins owned by a team. + `, + }, + } + ); +}; diff --git a/packages/kbn-plugin-check/jest.config.js b/packages/kbn-plugin-check/jest.config.js new file mode 100644 index 0000000000000..32cff56123cd7 --- /dev/null +++ b/packages/kbn-plugin-check/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-plugin-check'], +}; diff --git a/packages/kbn-plugin-check/kibana.jsonc b/packages/kbn-plugin-check/kibana.jsonc new file mode 100644 index 0000000000000..5081138fe5e2a --- /dev/null +++ b/packages/kbn-plugin-check/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/plugin-check", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/kbn-plugin-check/lib/get_all_plugins.ts b/packages/kbn-plugin-check/lib/get_all_plugins.ts new file mode 100644 index 0000000000000..33cc5155bdf3c --- /dev/null +++ b/packages/kbn-plugin-check/lib/get_all_plugins.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { findPlugins } from '@kbn/docs-utils'; +import { ToolingLog } from '@kbn/tooling-log'; + +/** + * Utility method for finding and logging information about all plugins. + */ +export const getAllPlugins = (log: ToolingLog) => { + const plugins = findPlugins().filter((plugin) => plugin.isPlugin); + log.info(`Found ${plugins.length} plugins.`); + log.debug('Found plugins:', plugins); + return plugins; +}; diff --git a/packages/kbn-plugin-check/lib/get_plugin.ts b/packages/kbn-plugin-check/lib/get_plugin.ts new file mode 100644 index 0000000000000..d346f646c1b7f --- /dev/null +++ b/packages/kbn-plugin-check/lib/get_plugin.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { findPlugins } from '@kbn/docs-utils'; +import { ToolingLog } from '@kbn/tooling-log'; + +/** + * Utility method for finding and logging information about a plugin. + */ +export const getPlugin = (pluginName: string, log: ToolingLog) => { + const plugin = findPlugins([pluginName])[0]; + log.debug('Found plugin:', pluginName); + return plugin; +}; diff --git a/packages/kbn-plugin-check/lib/get_plugin_classes.ts b/packages/kbn-plugin-check/lib/get_plugin_classes.ts new file mode 100644 index 0000000000000..d7e40b4a80930 --- /dev/null +++ b/packages/kbn-plugin-check/lib/get_plugin_classes.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Project } from 'ts-morph'; + +import { ToolingLog } from '@kbn/tooling-log'; +import { PluginOrPackage } from '@kbn/docs-utils/src/types'; + +/** + * Return the `client` and `server` plugin classes for a plugin. + */ +export const getPluginClasses = (project: Project, plugin: PluginOrPackage, log: ToolingLog) => { + // The `client` and `server` plugins have a consistent name and directory structure, but the + // `client` plugin _may_ be a `.tsx` file, so we need to check for both. + let client = project.getSourceFile(`${plugin.directory}/public/plugin.ts`); + + if (!client) { + client = project.getSourceFile(`${plugin.directory}/public/plugin.tsx`); + } + + const server = project.getSourceFile(`${plugin.directory}/server/plugin.ts`); + + // Log the warning if one or both plugin implementations are missing. + if (!client || !server) { + if (!client) { + log.warning(`${plugin.id}/client: no plugin.`); + } + + if (!server) { + log.warning(`${plugin.id}/server: no plugin.`); + } + } + + // We restrict files to a single class, so assigning the first element from the + // resulting array should be fine. + return { + project, + client: client ? client.getClasses()[0] : null, + server: server ? server.getClasses()[0] : null, + }; +}; diff --git a/packages/kbn-plugin-check/lib/index.ts b/packages/kbn-plugin-check/lib/index.ts new file mode 100644 index 0000000000000..55d1496823220 --- /dev/null +++ b/packages/kbn-plugin-check/lib/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getPlugin } from './get_plugin'; +export { getPluginClasses } from './get_plugin_classes'; +export { getAllPlugins } from './get_all_plugins'; diff --git a/packages/kbn-plugin-check/package.json b/packages/kbn-plugin-check/package.json new file mode 100644 index 0000000000000..2db46fce1c069 --- /dev/null +++ b/packages/kbn-plugin-check/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/plugin-check", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-plugin-check/rank.ts b/packages/kbn-plugin-check/rank.ts new file mode 100644 index 0000000000000..5089fd1deee7e --- /dev/null +++ b/packages/kbn-plugin-check/rank.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MultiBar, Presets } from 'cli-progress'; + +import { ToolingLog } from '@kbn/tooling-log'; + +import { getAllPlugins } from './lib'; + +interface Dependencies { + required: readonly string[]; + optional: readonly string[]; + bundles?: readonly string[]; +} + +const getSpaces = (size: number, count: number) => { + const length = count > 9 && count < 100 ? 2 : count < 10 ? 1 : 3; + return ' '.repeat(size - length); +}; + +export const rankDependencies = (log: ToolingLog) => { + const plugins = getAllPlugins(log); + + const pluginMap = new Map(); + const pluginRequired = new Map(); + const pluginOptional = new Map(); + const pluginBundles = new Map(); + let minWidth = 0; + + plugins.forEach((plugin) => { + pluginMap.set(plugin.manifest.id, { + required: plugin.manifest.requiredPlugins || [], + optional: plugin.manifest.optionalPlugins || [], + bundles: plugin.manifest.requiredBundles || [], + }); + + if (plugin.manifest.id.length > minWidth) { + minWidth = plugin.manifest.id.length; + } + }); + + pluginMap.forEach((dependencies) => { + dependencies.required.forEach((required) => { + pluginRequired.set(required, (pluginRequired.get(required) || 0) + 1); + }); + + dependencies.optional.forEach((optional) => { + pluginOptional.set(optional, (pluginOptional.get(optional) || 0) + 1); + }); + + dependencies.bundles?.forEach((bundle) => { + pluginBundles.set(bundle, (pluginBundles.get(bundle) || 0) + 1); + }); + }); + + const sorted = [...pluginMap.entries()].sort((a, b) => { + const aRequired = pluginRequired.get(a[0]) || 0; + const aOptional = pluginOptional.get(a[0]) || 0; + const aBundles = pluginBundles.get(a[0]) || 0; + const aTotal = aRequired + aOptional + aBundles; + + const bRequired = pluginRequired.get(b[0]) || 0; + const bOptional = pluginOptional.get(b[0]) || 0; + const bBundles = pluginBundles.get(b[0]) || 0; + const bTotal = bRequired + bOptional + bBundles; + + return bTotal - aTotal; + }); + + log.debug(`Ranking ${sorted.length} plugins.`); + + // sorted.forEach((plugin) => { + // log.info(`${plugin[0]}: ${plugin[1]}/${pluginOptional.get(plugin[0]) || 0}`); + // }); + + const multiBar = new MultiBar( + { + clearOnComplete: false, + hideCursor: true, + format: ' {bar} | {plugin} | {usage} | {info}', + }, + Presets.shades_grey + ); + + multiBar.create(sorted.length, sorted.length, { + plugin: `${sorted.length} plugins${' '.repeat(minWidth - 11)}`, + usage: 'total', + info: 'req opt bun', + }); + + sorted.forEach(([plugin]) => { + const total = sorted.length; + const optional = pluginOptional.get(plugin) || 0; + const required = pluginRequired.get(plugin) || 0; + const bundles = pluginBundles.get(plugin) || 0; + const usage = optional + required + bundles; + + multiBar.create(total, required + optional + bundles, { + plugin: `${plugin}${' '.repeat(minWidth - plugin.length)}`, + info: `${required}${getSpaces(4, required)}${optional}${getSpaces( + 4, + optional + )}${bundles}${getSpaces(4, bundles)}`, + usage: `${usage}${getSpaces(5, usage)}`, + }); + }); + + multiBar.stop(); + + return sorted; +}; diff --git a/packages/kbn-plugin-check/tsconfig.json b/packages/kbn-plugin-check/tsconfig.json new file mode 100644 index 0000000000000..3daf5600fa25d --- /dev/null +++ b/packages/kbn-plugin-check/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/tooling-log", + "@kbn/docs-utils", + "@kbn/dev-cli-runner", + ] +} diff --git a/packages/kbn-plugin-check/types.ts b/packages/kbn-plugin-check/types.ts new file mode 100644 index 0000000000000..739644f21cdcf --- /dev/null +++ b/packages/kbn-plugin-check/types.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ClassDeclaration, Project } from 'ts-morph'; +import { + PLUGIN_LAYERS, + PLUGIN_LIFECYCLES, + PLUGIN_REQUIREMENTS, + MANIFEST_STATES, + DEPENDENCY_STATES, + PLUGIN_STATES, + SOURCE_OF_TYPE, + MANIFEST_REQUIREMENTS, +} from './const'; + +/** An enumeration of plugin classes within a single plugin. */ +export type PluginLayer = typeof PLUGIN_LAYERS[number]; + +/** An enumeration of dependency requirements for a plugin. */ +export type PluginRequirement = typeof PLUGIN_REQUIREMENTS[number]; + +/** An enumeration of lifecycles a plugin should implement. */ +export type PluginLifecycle = typeof PLUGIN_LIFECYCLES[number]; + +/** An enumeration of manifest requirement states for a plugin dependency. */ +export type ManifestRequirement = typeof MANIFEST_REQUIREMENTS[number]; + +/** An enumeration of derived manifest states for a plugin dependency. */ +export type ManifestState = typeof MANIFEST_STATES[number]; + +/** An enumeration of derived plugin states for a dependency. */ +export type PluginState = typeof PLUGIN_STATES[number]; + +/** An enumeration of derived dependency states. */ +export type DependencyState = typeof DEPENDENCY_STATES[number]; + +/** An enumeration of where a type could be derived from a plugin class. */ +export type SourceOfType = typeof SOURCE_OF_TYPE[number]; + +/** Information about a given plugin. */ +export interface PluginInfo { + /** The unique Kibana identifier for the plugin. */ + name: string; + /** + * The `ts-morph` project for the plugin; this is expensive to create, so it is + * also stored here. + */ + project: Project; + /** Class Declarations from `ts-morph` for the plugin layers. */ + classes: { + /** Class Declarations from `ts-morph` for the `client` plugin layer. */ + client: ClassDeclaration | null; + /** Class Declarations from `ts-morph` for the `server` plugin layer. */ + server: ClassDeclaration | null; + }; + /** Dependencies and their states for the plugin. */ + dependencies: { + /** Dependencies derived from the manifest. */ + manifest: ManifestDependencies; + /** Dependencies derived from the plugin code. */ + plugin: PluginDependencies; + } & All; +} + +// Convenience type to include an `all` field of combined, unique dependency names +// for any given subset. +interface All { + all: Readonly; +} + +/** Dependencies organized by whether or not they are required. */ +export type Dependencies = { + [requirement in PluginRequirement]: Readonly; +} & { + /** From where the dependencies were derived-- e.g. a type or instance method. */ + source: SourceOfType; + /** The name of the type, if any. */ + typeName: string | null; +} & All; + +/** Dependencies organized by plugin lifecycle. */ +export type Lifecycle = { + [lifecycle in PluginLifecycle]: Dependencies | null; +} & All; + +/** Dependencies organized by plugin layer. */ +export type PluginDependencies = { + [layer in PluginLayer]: Lifecycle; +} & All; + +/** Dependencies organized by manifest requirement. */ +export type ManifestDependencies = { + [requirement in ManifestRequirement]: Readonly; +} & All; + +// The hierarchical representation of a plugin's dependencies: +// plugin layer -> lifecycle -> requirement -> dependency info +type PluginStatus = { + [layer in PluginLayer]: { + [lifecycle in PluginLifecycle]: { + typeName: string; + source: SourceOfType; + pluginState: PluginState; + }; + }; +}; + +/** A map of dependencies and their status organized by name. */ +export interface PluginStatuses { + [pluginId: string]: PluginStatus & { + status: DependencyState; + manifestState: ManifestState; + }; +} diff --git a/scripts/plugin_check.js b/scripts/plugin_check.js new file mode 100644 index 0000000000000..acdc92a9dfa9d --- /dev/null +++ b/scripts/plugin_check.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/plugin-check').runPluginCheckCli(); diff --git a/tsconfig.base.json b/tsconfig.base.json index a4f658e7acb62..29d5892f0ecaf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1168,6 +1168,8 @@ "@kbn/performance-testing-dataset-extractor/*": ["packages/kbn-performance-testing-dataset-extractor/*"], "@kbn/picomatcher": ["packages/kbn-picomatcher"], "@kbn/picomatcher/*": ["packages/kbn-picomatcher/*"], + "@kbn/plugin-check": ["packages/kbn-plugin-check"], + "@kbn/plugin-check/*": ["packages/kbn-plugin-check/*"], "@kbn/plugin-generator": ["packages/kbn-plugin-generator"], "@kbn/plugin-generator/*": ["packages/kbn-plugin-generator/*"], "@kbn/plugin-helpers": ["packages/kbn-plugin-helpers"], diff --git a/x-pack/plugins/apm/scripts/infer_route_return_types/index.ts b/x-pack/plugins/apm/scripts/infer_route_return_types/index.ts index c3fb30e2d17ac..ce4ba6a9b16df 100644 --- a/x-pack/plugins/apm/scripts/infer_route_return_types/index.ts +++ b/x-pack/plugins/apm/scripts/infer_route_return_types/index.ts @@ -99,10 +99,10 @@ files.forEach((file) => { .literal as ts.StringLiteral; // replace absolute paths with relative paths - return ts.updateImportTypeNode( + return ts.factory.updateImportTypeNode( node, - ts.createLiteralTypeNode( - ts.createStringLiteral( + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral( `./${Path.relative( Path.dirname(file.getFilePath()), literal.text diff --git a/yarn.lock b/yarn.lock index 45dce47febdcf..616b95f7ced75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5396,6 +5396,10 @@ version "0.0.0" uid "" +"@kbn/plugin-check@link:packages/kbn-plugin-check": + version "0.0.0" + uid "" + "@kbn/plugin-generator@link:packages/kbn-plugin-generator": version "0.0.0" uid "" @@ -8758,13 +8762,13 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@ts-morph/common@~0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.2.tgz#61d07a47d622d231e833c44471ab306faaa41aed" - integrity sha512-m5KjptpIf1K0t0QL38uE+ol1n+aNn9MgRq++G3Zym1FlqfN+rThsXlp3cAgib14pIeXF7jk3UtJQOviwawFyYg== +"@ts-morph/common@~0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.16.0.tgz#57e27d4b3fd65a4cd72cb36679ed08acb40fa3ba" + integrity sha512-SgJpzkTgZKLKqQniCjLaE3c2L2sdL7UShvmTmPBejAKd2OKV/yfMpQ2IWpAuA+VY5wy7PkSUaEObIqEK6afFuw== dependencies: - fast-glob "^3.2.7" - minimatch "^3.0.4" + fast-glob "^3.2.11" + minimatch "^5.1.0" mkdirp "^1.0.4" path-browserify "^1.0.1" @@ -16847,7 +16851,7 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.2: +fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -22405,7 +22409,7 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -29598,12 +29602,12 @@ ts-easing@^0.2.0: resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== -ts-morph@^13.0.2: - version "13.0.2" - resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.2.tgz#55546023493ef82389d9e4f28848a556c784bac4" - integrity sha512-SjeeHaRf/mFsNeR3KTJnx39JyEOzT4e+DX28gQx5zjzEOuFs2eGrqeN2PLKs/+AibSxPmzV7RD8nJVKmFJqtLA== +ts-morph@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-15.1.0.tgz#53deea5296d967ff6eba8f15f99d378aa7074a4e" + integrity sha512-RBsGE2sDzUXFTnv8Ba22QfeuKbgvAGJFuTN7HfmIRUkgT/NaVLfDM/8OFm2NlFkGlWEXdpW5OaFIp1jvqdDuOg== dependencies: - "@ts-morph/common" "~0.12.2" + "@ts-morph/common" "~0.16.0" code-block-writer "^11.0.0" ts-node@^10.9.1: