-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[dx] Dependency check script for plugins #171483
Merged
clintandrewhall
merged 7 commits into
elastic:main
from
clintandrewhall:util/dependency_check
Jan 17, 2024
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c73329b
[wip] Dependency check script for plugins
clintandrewhall a86d50a
Lower version of ts-morph to avoid breaking APM scripts.
clintandrewhall 6e98982
Update comments.
clintandrewhall 02a46b9
fix yarn.lock
clintandrewhall 6ace3b1
Add --rank and --dependents
clintandrewhall 07e52e6
Improve CLI arguments
clintandrewhall 906e9ed
Merge branch 'main' into util/dependency_check
clintandrewhall File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,5 @@ | |
*/ | ||
|
||
export { runBuildApiDocsCli } from './src'; | ||
|
||
export { findPlugins, findTeamPlugins } from './src/find_plugins'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"import/no-extraneous-dependencies": "off" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.