mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
Add ESLINT constraints to detect inter-group dependencies (#194810)
## Summary Addresses https://github.com/elastic/kibana-team/issues/1175 As part of the **Sustainable Kibana Architecture** initiative, this PR sets the foundation to start classifying plugins in isolated groups, matching our current solutions / project types: * It adds support for the following fields in the packages' manifests (kibana.jsonc): * `group?: 'search' | 'security' | 'observability' | 'platform' | 'common'` * `visibility?: 'private' | 'shared'` * It proposes a folder structure to automatically infer groups: ```javascript 'src/platform/plugins/shared': { group: 'platform', visibility: 'shared', }, 'src/platform/plugins/internal': { group: 'platform', visibility: 'private', }, 'x-pack/platform/plugins/shared': { group: 'platform', visibility: 'shared', }, 'x-pack/platform/plugins/internal': { group: 'platform', visibility: 'private', }, 'x-pack/solutions/observability/plugins': { group: 'observability', visibility: 'private', }, 'x-pack/solutions/security/plugins': { group: 'security', visibility: 'private', }, 'x-pack/solutions/search/plugins': { group: 'search', visibility: 'private', }, ``` * If a plugin is moved to one of the specific locations above, the group and visibility in the manifest (if specified) must match those inferred from the path. * Plugins that are not relocated are considered: `group: 'common', visibility: 'shared'` by default. As soon as we specify a custom `group`, the ESLINT rules will check violations against dependencies / dependants. The ESLINT rules are pretty simple: * Plugins can only depend on: * Plugins in the same group * OR plugins with `'shared'` visibility * Plugins in `'observability', 'security', 'search'` groups are mandatorily `'private'`. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
300678ca85
commit
2a085e103a
36 changed files with 1278 additions and 49 deletions
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
|
@ -597,6 +597,7 @@ packages/kbn-management/settings/types @elastic/kibana-management
|
||||||
packages/kbn-management/settings/utilities @elastic/kibana-management
|
packages/kbn-management/settings/utilities @elastic/kibana-management
|
||||||
packages/kbn-management/storybook/config @elastic/kibana-management
|
packages/kbn-management/storybook/config @elastic/kibana-management
|
||||||
test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management
|
test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management
|
||||||
|
packages/kbn-manifest @elastic/kibana-core
|
||||||
packages/kbn-mapbox-gl @elastic/kibana-presentation
|
packages/kbn-mapbox-gl @elastic/kibana-presentation
|
||||||
x-pack/examples/third_party_maps_source_example @elastic/kibana-presentation
|
x-pack/examples/third_party_maps_source_example @elastic/kibana-presentation
|
||||||
src/plugins/maps_ems @elastic/kibana-presentation
|
src/plugins/maps_ems @elastic/kibana-presentation
|
||||||
|
@ -929,9 +930,9 @@ packages/kbn-test-eui-helpers @elastic/kibana-visualizations
|
||||||
x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security
|
x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security
|
||||||
packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa
|
packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa
|
||||||
packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa
|
packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa
|
||||||
x-pack/test_serverless
|
x-pack/test_serverless
|
||||||
test
|
test
|
||||||
x-pack/test
|
x-pack/test
|
||||||
x-pack/performance @elastic/appex-qa
|
x-pack/performance @elastic/appex-qa
|
||||||
x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations
|
x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations
|
||||||
x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations
|
x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations
|
||||||
|
|
|
@ -635,6 +635,7 @@
|
||||||
"@kbn/management-settings-types": "link:packages/kbn-management/settings/types",
|
"@kbn/management-settings-types": "link:packages/kbn-management/settings/types",
|
||||||
"@kbn/management-settings-utilities": "link:packages/kbn-management/settings/utilities",
|
"@kbn/management-settings-utilities": "link:packages/kbn-management/settings/utilities",
|
||||||
"@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin",
|
"@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin",
|
||||||
|
"@kbn/manifest": "link:packages/kbn-manifest",
|
||||||
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
|
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
|
||||||
"@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example",
|
"@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example",
|
||||||
"@kbn/maps-ems-plugin": "link:src/plugins/maps_ems",
|
"@kbn/maps-ems-plugin": "link:src/plugins/maps_ems",
|
||||||
|
|
|
@ -20,14 +20,39 @@ const OUTPUT_PATH = Path.resolve(REPO_ROOT, 'docs/developer/plugin-list.asciidoc
|
||||||
export function runPluginListCli() {
|
export function runPluginListCli() {
|
||||||
run(async ({ log }) => {
|
run(async ({ log }) => {
|
||||||
log.info('looking for oss plugins');
|
log.info('looking for oss plugins');
|
||||||
const ossPlugins = discoverPlugins('src/plugins');
|
const ossLegacyPlugins = discoverPlugins('src/plugins');
|
||||||
log.success(`found ${ossPlugins.length} plugins`);
|
const ossPlatformPlugins = discoverPlugins('src/platform/plugins');
|
||||||
|
log.success(`found ${ossLegacyPlugins.length + ossPlatformPlugins.length} plugins`);
|
||||||
|
|
||||||
log.info('looking for x-pack plugins');
|
log.info('looking for x-pack plugins');
|
||||||
const xpackPlugins = discoverPlugins('x-pack/plugins');
|
const xpackLegacyPlugins = discoverPlugins('x-pack/plugins');
|
||||||
log.success(`found ${xpackPlugins.length} plugins`);
|
const xpackPlatformPlugins = discoverPlugins('x-pack/platform/plugins');
|
||||||
|
const xpackSearchPlugins = discoverPlugins('x-pack/solutions/search/plugins');
|
||||||
|
const xpackSecurityPlugins = discoverPlugins('x-pack/solutions/security/plugins');
|
||||||
|
const xpackObservabilityPlugins = discoverPlugins('x-pack/solutions/observability/plugins');
|
||||||
|
log.success(
|
||||||
|
`found ${
|
||||||
|
xpackLegacyPlugins.length +
|
||||||
|
xpackPlatformPlugins.length +
|
||||||
|
xpackSearchPlugins.length +
|
||||||
|
xpackSecurityPlugins.length +
|
||||||
|
xpackObservabilityPlugins.length
|
||||||
|
} plugins`
|
||||||
|
);
|
||||||
|
|
||||||
log.info('writing plugin list to', OUTPUT_PATH);
|
log.info('writing plugin list to', OUTPUT_PATH);
|
||||||
Fs.writeFileSync(OUTPUT_PATH, generatePluginList(ossPlugins, xpackPlugins));
|
Fs.writeFileSync(
|
||||||
|
OUTPUT_PATH,
|
||||||
|
generatePluginList(
|
||||||
|
[...ossLegacyPlugins, ...ossPlatformPlugins],
|
||||||
|
[
|
||||||
|
...xpackLegacyPlugins,
|
||||||
|
...xpackPlatformPlugins,
|
||||||
|
...xpackSearchPlugins,
|
||||||
|
...xpackSecurityPlugins,
|
||||||
|
...xpackObservabilityPlugins,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,6 +317,7 @@ module.exports = {
|
||||||
'@kbn/disable/no_naked_eslint_disable': 'error',
|
'@kbn/disable/no_naked_eslint_disable': 'error',
|
||||||
'@kbn/eslint/no_async_promise_body': 'error',
|
'@kbn/eslint/no_async_promise_body': 'error',
|
||||||
'@kbn/eslint/no_async_foreach': 'error',
|
'@kbn/eslint/no_async_foreach': 'error',
|
||||||
|
'@kbn/eslint/no_deprecated_authz_config': 'error',
|
||||||
'@kbn/eslint/no_trailing_import_slash': 'error',
|
'@kbn/eslint/no_trailing_import_slash': 'error',
|
||||||
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
|
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
|
||||||
'@kbn/eslint/no_this_in_property_initializers': 'error',
|
'@kbn/eslint/no_this_in_property_initializers': 'error',
|
||||||
|
@ -326,8 +327,8 @@ module.exports = {
|
||||||
'@kbn/imports/uniform_imports': 'error',
|
'@kbn/imports/uniform_imports': 'error',
|
||||||
'@kbn/imports/no_unused_imports': 'error',
|
'@kbn/imports/no_unused_imports': 'error',
|
||||||
'@kbn/imports/no_boundary_crossing': 'error',
|
'@kbn/imports/no_boundary_crossing': 'error',
|
||||||
'@kbn/eslint/no_deprecated_authz_config': 'error',
|
'@kbn/imports/no_group_crossing_manifests': 'error',
|
||||||
|
'@kbn/imports/no_group_crossing_imports': 'error',
|
||||||
'no-new-func': 'error',
|
'no-new-func': 'error',
|
||||||
'no-implied-eval': 'error',
|
'no-implied-eval': 'error',
|
||||||
'no-prototype-builtins': 'error',
|
'no-prototype-builtins': 'error',
|
||||||
|
|
|
@ -12,4 +12,6 @@ export const PROTECTED_RULES = new Set([
|
||||||
'@kbn/disable/no_protected_eslint_disable',
|
'@kbn/disable/no_protected_eslint_disable',
|
||||||
'@kbn/disable/no_naked_eslint_disable',
|
'@kbn/disable/no_naked_eslint_disable',
|
||||||
'@kbn/imports/no_unused_imports',
|
'@kbn/imports/no_unused_imports',
|
||||||
|
'@kbn/imports/no_group_crossing_imports',
|
||||||
|
'@kbn/imports/no_group_crossing_manifests',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { UniformImportsRule } from './src/rules/uniform_imports';
|
||||||
import { ExportsMovedPackagesRule } from './src/rules/exports_moved_packages';
|
import { ExportsMovedPackagesRule } from './src/rules/exports_moved_packages';
|
||||||
import { NoUnusedImportsRule } from './src/rules/no_unused_imports';
|
import { NoUnusedImportsRule } from './src/rules/no_unused_imports';
|
||||||
import { NoBoundaryCrossingRule } from './src/rules/no_boundary_crossing';
|
import { NoBoundaryCrossingRule } from './src/rules/no_boundary_crossing';
|
||||||
|
import { NoGroupCrossingImportsRule } from './src/rules/no_group_crossing_imports';
|
||||||
|
import { NoGroupCrossingManifestsRule } from './src/rules/no_group_crossing_manifests';
|
||||||
import { RequireImportRule } from './src/rules/require_import';
|
import { RequireImportRule } from './src/rules/require_import';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,5 +27,7 @@ export const rules = {
|
||||||
exports_moved_packages: ExportsMovedPackagesRule,
|
exports_moved_packages: ExportsMovedPackagesRule,
|
||||||
no_unused_imports: NoUnusedImportsRule,
|
no_unused_imports: NoUnusedImportsRule,
|
||||||
no_boundary_crossing: NoBoundaryCrossingRule,
|
no_boundary_crossing: NoBoundaryCrossingRule,
|
||||||
|
no_group_crossing_imports: NoGroupCrossingImportsRule,
|
||||||
|
no_group_crossing_manifests: NoGroupCrossingManifestsRule,
|
||||||
require_import: RequireImportRule,
|
require_import: RequireImportRule,
|
||||||
};
|
};
|
||||||
|
|
25
packages/kbn-eslint-plugin-imports/src/helpers/groups.ts
Normal file
25
packages/kbn-eslint-plugin-imports/src/helpers/groups.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a given ModuleGroup can import from another one
|
||||||
|
* @param importerGroup The group of the module that we are checking
|
||||||
|
* @param importedGroup The group of the imported module
|
||||||
|
* @param importedVisibility The visibility of the imported module
|
||||||
|
* @returns true if importerGroup is allowed to import from importedGroup/Visibiliy
|
||||||
|
*/
|
||||||
|
export function isImportableFrom(
|
||||||
|
importerGroup: ModuleGroup,
|
||||||
|
importedGroup: ModuleGroup,
|
||||||
|
importedVisibility: ModuleVisibility
|
||||||
|
): boolean {
|
||||||
|
return importerGroup === importedGroup || importedVisibility === 'shared';
|
||||||
|
}
|
|
@ -30,3 +30,19 @@ export function report(context: Rule.RuleContext, options: ReportOptions) {
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toList = (strings: string[]) => {
|
||||||
|
const items = strings.map((s) => `"${s}"`);
|
||||||
|
const list = items.slice(0, -1).join(', ');
|
||||||
|
const last = items.at(-1);
|
||||||
|
return !list.length ? last ?? '' : `${list} or ${last}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatSuggestions = (suggestions: string[]) => {
|
||||||
|
const s = suggestions.map((l) => l.trim()).filter(Boolean);
|
||||||
|
if (!s.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` \nSuggestions:\n - ${s.join('\n - ')}\n\n`;
|
||||||
|
};
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
|
|
||||||
import { RuleTester } from 'eslint';
|
import { RuleTester } from 'eslint';
|
||||||
import { NoBoundaryCrossingRule } from './no_boundary_crossing';
|
import { NoBoundaryCrossingRule } from './no_boundary_crossing';
|
||||||
import { ModuleType } from '@kbn/repo-source-classifier';
|
import type { ModuleType } from '@kbn/repo-source-classifier';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
|
import { formatSuggestions } from '../helpers/report';
|
||||||
|
|
||||||
const make = (from: ModuleType, to: ModuleType, imp = 'import') => ({
|
const make = (from: ModuleType, to: ModuleType, imp = 'import') => ({
|
||||||
filename: `${from}.ts`,
|
filename: `${from}.ts`,
|
||||||
|
@ -107,13 +108,12 @@ for (const [name, tester] of [tsTester, babelTester]) {
|
||||||
data: {
|
data: {
|
||||||
importedType: 'server package',
|
importedType: 'server package',
|
||||||
ownType: 'common package',
|
ownType: 'common package',
|
||||||
suggestion: ` ${dedent`
|
suggestion: formatSuggestions([
|
||||||
Suggestions:
|
'Remove the import statement.',
|
||||||
- Remove the import statement.
|
'Limit your imports to "common package" or "static" code.',
|
||||||
- Limit your imports to "common package" or "static" code.
|
'Covert to a type-only import.',
|
||||||
- Covert to a type-only import.
|
'Reach out to #kibana-operations for help.',
|
||||||
- Reach out to #kibana-operations for help.
|
]),
|
||||||
`}`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -12,13 +12,14 @@ import Path from 'path';
|
||||||
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
||||||
import * as Bt from '@babel/types';
|
import * as Bt from '@babel/types';
|
||||||
import type { Rule } from 'eslint';
|
import type { Rule } from 'eslint';
|
||||||
import ESTree from 'estree';
|
import type { Node } from 'estree';
|
||||||
import { ModuleType } from '@kbn/repo-source-classifier';
|
import type { ModuleType } from '@kbn/repo-source-classifier';
|
||||||
|
|
||||||
import { visitAllImportStatements, Importer } from '../helpers/visit_all_import_statements';
|
import { visitAllImportStatements, Importer } from '../helpers/visit_all_import_statements';
|
||||||
import { getSourcePath } from '../helpers/source';
|
import { getSourcePath } from '../helpers/source';
|
||||||
import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
|
import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
|
||||||
import { getImportResolver } from '../get_import_resolver';
|
import { getImportResolver } from '../get_import_resolver';
|
||||||
|
import { formatSuggestions, toList } from '../helpers/report';
|
||||||
|
|
||||||
const ANY = Symbol();
|
const ANY = Symbol();
|
||||||
|
|
||||||
|
@ -33,22 +34,6 @@ const IMPORTABLE_FROM: Record<ModuleType, ModuleType[] | typeof ANY> = {
|
||||||
tooling: ANY,
|
tooling: ANY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toList = (strings: string[]) => {
|
|
||||||
const items = strings.map((s) => `"${s}"`);
|
|
||||||
const list = items.slice(0, -1).join(', ');
|
|
||||||
const last = items.at(-1);
|
|
||||||
return !list.length ? last ?? '' : `${list} or ${last}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSuggestions = (suggestions: string[]) => {
|
|
||||||
const s = suggestions.map((l) => l.trim()).filter(Boolean);
|
|
||||||
if (!s.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return ` Suggestions:\n - ${s.join('\n - ')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTypeOnlyImport = (importer: Importer) => {
|
const isTypeOnlyImport = (importer: Importer) => {
|
||||||
// handle babel nodes
|
// handle babel nodes
|
||||||
if (Bt.isImportDeclaration(importer)) {
|
if (Bt.isImportDeclaration(importer)) {
|
||||||
|
@ -125,7 +110,7 @@ export const NoBoundaryCrossingRule: Rule.RuleModule = {
|
||||||
|
|
||||||
if (!importable.includes(imported.type)) {
|
if (!importable.includes(imported.type)) {
|
||||||
context.report({
|
context.report({
|
||||||
node: node as ESTree.Node,
|
node: node as Node,
|
||||||
messageId: 'TYPE_MISMATCH',
|
messageId: 'TYPE_MISMATCH',
|
||||||
data: {
|
data: {
|
||||||
ownType: self.type,
|
ownType: self.type,
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RuleTester } from 'eslint';
|
||||||
|
import dedent from 'dedent';
|
||||||
|
import { NoGroupCrossingImportsRule } from './no_group_crossing_imports';
|
||||||
|
import { formatSuggestions } from '../helpers/report';
|
||||||
|
import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
|
||||||
|
const make = (
|
||||||
|
fromGroup: ModuleGroup,
|
||||||
|
fromVisibility: ModuleVisibility,
|
||||||
|
toGroup: ModuleGroup,
|
||||||
|
toVisibility: ModuleVisibility,
|
||||||
|
imp = 'import'
|
||||||
|
) => ({
|
||||||
|
filename: `${fromGroup}.${fromVisibility}.ts`,
|
||||||
|
code: dedent`
|
||||||
|
${imp} '${toGroup}.${toVisibility}'
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../get_import_resolver', () => {
|
||||||
|
return {
|
||||||
|
getImportResolver() {
|
||||||
|
return {
|
||||||
|
resolve(req: string) {
|
||||||
|
return {
|
||||||
|
type: 'file',
|
||||||
|
absolute: req.split('.'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../helpers/repo_source_classifier', () => {
|
||||||
|
return {
|
||||||
|
getRepoSourceClassifier() {
|
||||||
|
return {
|
||||||
|
classify(r: string | [string, string]) {
|
||||||
|
const [group, visibility] =
|
||||||
|
typeof r === 'string' ? (r.endsWith('.ts') ? r.slice(0, -3) : r).split('.') : r;
|
||||||
|
return {
|
||||||
|
pkgInfo: {
|
||||||
|
pkgId: 'aPackage',
|
||||||
|
},
|
||||||
|
group,
|
||||||
|
visibility,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tsTester = [
|
||||||
|
'@typescript-eslint/parser',
|
||||||
|
new RuleTester({
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const babelTester = [
|
||||||
|
'@babel/eslint-parser',
|
||||||
|
new RuleTester({
|
||||||
|
parser: require.resolve('@babel/eslint-parser'),
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
presets: ['@kbn/babel-preset/node_preset'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const [name, tester] of [tsTester, babelTester]) {
|
||||||
|
describe(name, () => {
|
||||||
|
tester.run('@kbn/imports/no_group_crossing_imports', NoGroupCrossingImportsRule, {
|
||||||
|
valid: [
|
||||||
|
make('observability', 'private', 'observability', 'private'),
|
||||||
|
make('security', 'private', 'security', 'private'),
|
||||||
|
make('search', 'private', 'search', 'private'),
|
||||||
|
make('observability', 'private', 'platform', 'shared'),
|
||||||
|
make('security', 'private', 'common', 'shared'),
|
||||||
|
make('platform', 'shared', 'platform', 'shared'),
|
||||||
|
make('platform', 'shared', 'platform', 'private'),
|
||||||
|
make('common', 'shared', 'common', 'shared'),
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
...make('observability', 'private', 'security', 'private'),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 1,
|
||||||
|
messageId: 'ILLEGAL_IMPORT',
|
||||||
|
data: {
|
||||||
|
importerPackage: 'aPackage',
|
||||||
|
importerGroup: 'observability',
|
||||||
|
importedPackage: 'aPackage',
|
||||||
|
importedGroup: 'security',
|
||||||
|
importedVisibility: 'private',
|
||||||
|
sourcePath: 'observability.private.ts',
|
||||||
|
suggestion: formatSuggestions([
|
||||||
|
`Please review the dependencies in your module's manifest (kibana.jsonc).`,
|
||||||
|
`Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
|
||||||
|
`Address the conflicting dependencies by refactoring the code`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...make('security', 'private', 'platform', 'private'),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 1,
|
||||||
|
messageId: 'ILLEGAL_IMPORT',
|
||||||
|
data: {
|
||||||
|
importerPackage: 'aPackage',
|
||||||
|
importerGroup: 'security',
|
||||||
|
importedPackage: 'aPackage',
|
||||||
|
importedGroup: 'platform',
|
||||||
|
importedVisibility: 'private',
|
||||||
|
sourcePath: 'security.private.ts',
|
||||||
|
suggestion: formatSuggestions([
|
||||||
|
`Please review the dependencies in your module's manifest (kibana.jsonc).`,
|
||||||
|
`Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
|
||||||
|
`Address the conflicting dependencies by refactoring the code`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
import type { Node } from 'estree';
|
||||||
|
import { REPO_ROOT } from '@kbn/repo-info';
|
||||||
|
|
||||||
|
import { visitAllImportStatements } from '../helpers/visit_all_import_statements';
|
||||||
|
import { getSourcePath } from '../helpers/source';
|
||||||
|
import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
|
||||||
|
import { getImportResolver } from '../get_import_resolver';
|
||||||
|
import { formatSuggestions } from '../helpers/report';
|
||||||
|
import { isImportableFrom } from '../helpers/groups';
|
||||||
|
|
||||||
|
export const NoGroupCrossingImportsRule: Rule.RuleModule = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports',
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
ILLEGAL_IMPORT: `⚠ Illegal import statement: "{{importerPackage}}" ({{importerGroup}}) is importing "{{importedPackage}}" ({{importedGroup}}/{{importedVisibility}}). File: {{sourcePath}}\n{{suggestion}}\n`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const resolver = getImportResolver(context);
|
||||||
|
const classifier = getRepoSourceClassifier(resolver);
|
||||||
|
const sourcePath = getSourcePath(context);
|
||||||
|
const ownDirname = dirname(sourcePath);
|
||||||
|
const self = classifier.classify(sourcePath);
|
||||||
|
const relativePath = sourcePath.replace(REPO_ROOT, '').replace(/^\//, '');
|
||||||
|
|
||||||
|
return visitAllImportStatements((req, { node }) => {
|
||||||
|
if (
|
||||||
|
req === null ||
|
||||||
|
// we can ignore imports using the raw-loader, they will need to be resolved but can be managed on a case by case basis
|
||||||
|
req.startsWith('!!raw-loader')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = resolver.resolve(req, ownDirname);
|
||||||
|
if (result?.type !== 'file' || result.nodeModule) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imported = classifier.classify(result.absolute);
|
||||||
|
|
||||||
|
if (!isImportableFrom(self.group, imported.group, imported.visibility)) {
|
||||||
|
context.report({
|
||||||
|
node: node as Node,
|
||||||
|
messageId: 'ILLEGAL_IMPORT',
|
||||||
|
data: {
|
||||||
|
importerPackage: self.pkgInfo?.pkgId ?? 'unknown',
|
||||||
|
importerGroup: self.group,
|
||||||
|
importedPackage: imported.pkgInfo?.pkgId ?? 'unknown',
|
||||||
|
importedGroup: imported.group,
|
||||||
|
importedVisibility: imported.visibility,
|
||||||
|
sourcePath: relativePath,
|
||||||
|
suggestion: formatSuggestions([
|
||||||
|
`Please review the dependencies in your module's manifest (kibana.jsonc).`,
|
||||||
|
`Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
|
||||||
|
`Address the conflicting dependencies by refactoring the code`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,280 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RuleTester } from 'eslint';
|
||||||
|
import dedent from 'dedent';
|
||||||
|
import { NoGroupCrossingManifestsRule } from './no_group_crossing_manifests';
|
||||||
|
import { formatSuggestions } from '../helpers/report';
|
||||||
|
import { ModuleId } from '@kbn/repo-source-classifier/src/module_id';
|
||||||
|
import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
|
||||||
|
const makePlugin = (filename: string) => ({
|
||||||
|
filename,
|
||||||
|
code: dedent`
|
||||||
|
export function plugin() {
|
||||||
|
return new MyPlugin();
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makePluginClass = (filename: string) => ({
|
||||||
|
filename,
|
||||||
|
code: dedent`
|
||||||
|
class MyPlugin implements Plugin {
|
||||||
|
setup() {
|
||||||
|
console.log('foo');
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
console.log('foo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeModuleByPath = (
|
||||||
|
path: string,
|
||||||
|
group: ModuleGroup,
|
||||||
|
visibility: ModuleVisibility,
|
||||||
|
pluginOverrides: any = {}
|
||||||
|
): Record<string, ModuleId> => {
|
||||||
|
const pluginId = path.split('/')[4];
|
||||||
|
const packageId = `@kbn/${pluginId}-plugin`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
[path]: {
|
||||||
|
type: 'server package',
|
||||||
|
dirs: [],
|
||||||
|
repoRel: 'some/relative/path',
|
||||||
|
pkgInfo: {
|
||||||
|
pkgId: packageId,
|
||||||
|
pkgDir: path.split('/').slice(0, -2).join('/'),
|
||||||
|
rel: 'some/relative/path',
|
||||||
|
},
|
||||||
|
group,
|
||||||
|
visibility,
|
||||||
|
manifest: {
|
||||||
|
type: 'plugin',
|
||||||
|
id: packageId,
|
||||||
|
owner: ['@kbn/kibana-operations'],
|
||||||
|
plugin: {
|
||||||
|
id: pluginId,
|
||||||
|
browser: true,
|
||||||
|
server: true,
|
||||||
|
...pluginOverrides,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeError = (line: number, ...violations: string[]) => ({
|
||||||
|
line,
|
||||||
|
messageId: 'ILLEGAL_MANIFEST_DEPENDENCY',
|
||||||
|
data: {
|
||||||
|
violations: violations.join('\n'),
|
||||||
|
suggestion: formatSuggestions([
|
||||||
|
`Please review the dependencies in your plugin's manifest (kibana.jsonc).`,
|
||||||
|
`Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
|
||||||
|
`Address the conflicting dependencies by refactoring the code`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../helpers/repo_source_classifier', () => {
|
||||||
|
const MODULES_BY_PATH: Record<string, ModuleId> = {
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/search/plugins/searchPlugin1/server/index.ts',
|
||||||
|
'search',
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
requiredPlugins: ['searchPlugin2'], // allowed, same group
|
||||||
|
}
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/search/plugins/searchPlugin2/server/index.ts',
|
||||||
|
'search',
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
requiredPlugins: ['securityPlugin1'], // invalid, dependency belongs to another group
|
||||||
|
}
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/security/plugins/securityPlugin1/server/index.ts',
|
||||||
|
'security',
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
requiredPlugins: ['securityPlugin2'], // allowed, same group
|
||||||
|
}
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/security/plugins/securityPlugin2/server/index.ts',
|
||||||
|
'security',
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
requiredPlugins: ['platformPlugin1', 'platformPlugin2', 'platformPlugin3'], // 3rd one is private!
|
||||||
|
}
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/platform/shared/platformPlugin1/server/index.ts',
|
||||||
|
'platform',
|
||||||
|
'shared',
|
||||||
|
{
|
||||||
|
requiredPlugins: ['platformPlugin2', 'platformPlugin3', 'platformPlugin4'],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/platform/shared/platformPlugin2/server/index.ts',
|
||||||
|
'platform',
|
||||||
|
'shared'
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/platform/private/platformPlugin3/server/index.ts',
|
||||||
|
'platform',
|
||||||
|
'private'
|
||||||
|
),
|
||||||
|
...makeModuleByPath(
|
||||||
|
'path/to/platform/private/platformPlugin4/server/index.ts',
|
||||||
|
'platform',
|
||||||
|
'private'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRepoSourceClassifier() {
|
||||||
|
return {
|
||||||
|
classify(path: string) {
|
||||||
|
return MODULES_BY_PATH[path];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@kbn/repo-packages', () => {
|
||||||
|
const original = jest.requireActual('@kbn/repo-packages');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
getPluginPackagesFilter: () => () => true,
|
||||||
|
getPackages() {
|
||||||
|
return [
|
||||||
|
'path/to/search/plugins/searchPlugin1/server/index.ts',
|
||||||
|
'path/to/search/plugins/searchPlugin2/server/index.ts',
|
||||||
|
'path/to/security/plugins/securityPlugin1/server/index.ts',
|
||||||
|
'path/to/security/plugins/securityPlugin2/server/index.ts',
|
||||||
|
'path/to/platform/shared/platformPlugin1/server/index.ts',
|
||||||
|
'path/to/platform/shared/platformPlugin2/server/index.ts',
|
||||||
|
'path/to/platform/private/platformPlugin3/server/index.ts',
|
||||||
|
'path/to/platform/private/platformPlugin4/server/index.ts',
|
||||||
|
].map((path) => {
|
||||||
|
const [, , group, , id] = path.split('/');
|
||||||
|
return {
|
||||||
|
id: `@kbn/${id}-plugin`,
|
||||||
|
group,
|
||||||
|
visibility: path.includes('platform/shared') ? 'shared' : 'private',
|
||||||
|
manifest: {
|
||||||
|
plugin: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tsTester = [
|
||||||
|
'@typescript-eslint/parser',
|
||||||
|
new RuleTester({
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const babelTester = [
|
||||||
|
'@babel/eslint-parser',
|
||||||
|
new RuleTester({
|
||||||
|
parser: require.resolve('@babel/eslint-parser'),
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
presets: ['@kbn/babel-preset/node_preset'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const [name, tester] of [tsTester, babelTester]) {
|
||||||
|
describe(name, () => {
|
||||||
|
tester.run('@kbn/imports/no_group_crossing_manifests', NoGroupCrossingManifestsRule, {
|
||||||
|
valid: [
|
||||||
|
makePlugin('path/to/search/plugins/searchPlugin1/server/index.ts'),
|
||||||
|
makePlugin('path/to/security/plugins/securityPlugin1/server/index.ts'),
|
||||||
|
makePlugin('path/to/platform/shared/platformPlugin1/server/index.ts'),
|
||||||
|
makePluginClass('path/to/search/plugins/searchPlugin1/server/index.ts'),
|
||||||
|
makePluginClass('path/to/security/plugins/securityPlugin1/server/index.ts'),
|
||||||
|
makePluginClass('path/to/platform/shared/platformPlugin1/server/index.ts'),
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
...makePlugin('path/to/search/plugins/searchPlugin2/server/index.ts'),
|
||||||
|
errors: [
|
||||||
|
makeError(
|
||||||
|
1,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...makePlugin('path/to/security/plugins/securityPlugin2/server/index.ts'),
|
||||||
|
errors: [
|
||||||
|
makeError(
|
||||||
|
1,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...makePluginClass('path/to/search/plugins/searchPlugin2/server/index.ts'),
|
||||||
|
errors: [
|
||||||
|
makeError(
|
||||||
|
2,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
makeError(
|
||||||
|
5,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...makePluginClass('path/to/security/plugins/securityPlugin2/server/index.ts'),
|
||||||
|
errors: [
|
||||||
|
makeError(
|
||||||
|
2,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
makeError(
|
||||||
|
5,
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
import type { Node } from 'estree';
|
||||||
|
import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages';
|
||||||
|
import { REPO_ROOT } from '@kbn/repo-info';
|
||||||
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
import { getSourcePath } from '../helpers/source';
|
||||||
|
import { getImportResolver } from '../get_import_resolver';
|
||||||
|
import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
|
||||||
|
import { isImportableFrom } from '../helpers/groups';
|
||||||
|
import { formatSuggestions } from '../helpers/report';
|
||||||
|
|
||||||
|
const NODE_TYPES = TSESTree.AST_NODE_TYPES;
|
||||||
|
|
||||||
|
interface PluginInfo {
|
||||||
|
id: string;
|
||||||
|
pluginId: string;
|
||||||
|
group: ModuleGroup;
|
||||||
|
visibility: ModuleVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoGroupCrossingManifestsRule: Rule.RuleModule = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports',
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
ILLEGAL_MANIFEST_DEPENDENCY: `{{violations}}\n{{suggestion}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const sourcePath = getSourcePath(context);
|
||||||
|
let manifestPath: string;
|
||||||
|
const resolver = getImportResolver(context);
|
||||||
|
const classifier = getRepoSourceClassifier(resolver);
|
||||||
|
const moduleId = classifier.classify(sourcePath);
|
||||||
|
const offendingDependencies: PluginInfo[] = [];
|
||||||
|
let currentPlugin: PluginInfo;
|
||||||
|
|
||||||
|
if (moduleId.manifest?.type === 'plugin') {
|
||||||
|
manifestPath = join(moduleId.pkgInfo!.pkgDir, 'kibana.jsonc')
|
||||||
|
.replace(REPO_ROOT, '')
|
||||||
|
.replace(/^\//, '');
|
||||||
|
currentPlugin = {
|
||||||
|
id: moduleId.pkgInfo!.pkgId,
|
||||||
|
pluginId: moduleId.manifest.plugin.id,
|
||||||
|
group: moduleId.group,
|
||||||
|
visibility: moduleId.visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
const allPlugins = getPackages(REPO_ROOT).filter(getPluginPackagesFilter());
|
||||||
|
const currentPluginInfo = moduleId.manifest!.plugin;
|
||||||
|
// check all the dependencies in the manifest, looking for plugin violations
|
||||||
|
[
|
||||||
|
...(currentPluginInfo.requiredPlugins ?? []),
|
||||||
|
...(currentPluginInfo.requiredBundles ?? []),
|
||||||
|
...(currentPluginInfo.optionalPlugins ?? []),
|
||||||
|
...(currentPluginInfo.runtimePluginDependencies ?? []),
|
||||||
|
].forEach((pluginId) => {
|
||||||
|
const dependency = allPlugins.find(({ manifest }) => manifest.plugin.id === pluginId);
|
||||||
|
if (dependency) {
|
||||||
|
// at this point, we know the dependency is a plugin
|
||||||
|
const { id, group, visibility } = dependency;
|
||||||
|
if (!isImportableFrom(moduleId.group, group, visibility)) {
|
||||||
|
offendingDependencies.push({ id, pluginId, group, visibility });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
FunctionDeclaration(node) {
|
||||||
|
// complain in exported plugin() function
|
||||||
|
if (
|
||||||
|
currentPlugin &&
|
||||||
|
offendingDependencies.length &&
|
||||||
|
node.id?.name === 'plugin' &&
|
||||||
|
node.parent.type === NODE_TYPES.ExportNamedDeclaration
|
||||||
|
) {
|
||||||
|
reportViolation({
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
currentPlugin,
|
||||||
|
manifestPath,
|
||||||
|
offendingDependencies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MethodDefinition(node) {
|
||||||
|
// complain in setup() and start() hooks
|
||||||
|
if (
|
||||||
|
offendingDependencies.length &&
|
||||||
|
node.key.type === NODE_TYPES.Identifier &&
|
||||||
|
(node.key.name === 'setup' || node.key.name === 'start') &&
|
||||||
|
node.kind === 'method' &&
|
||||||
|
node.parent.parent.type === NODE_TYPES.ClassDeclaration &&
|
||||||
|
(node.parent.parent.id?.name.includes('Plugin') ||
|
||||||
|
(node.parent.parent as TSESTree.ClassDeclaration).implements?.find(
|
||||||
|
(value) =>
|
||||||
|
value.expression.type === NODE_TYPES.Identifier &&
|
||||||
|
value.expression.name === 'Plugin'
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
reportViolation({
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
currentPlugin,
|
||||||
|
manifestPath,
|
||||||
|
offendingDependencies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReportViolationParams {
|
||||||
|
context: Rule.RuleContext;
|
||||||
|
node: Node;
|
||||||
|
currentPlugin: PluginInfo;
|
||||||
|
offendingDependencies: PluginInfo[];
|
||||||
|
manifestPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportViolation = ({
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
currentPlugin,
|
||||||
|
offendingDependencies,
|
||||||
|
manifestPath,
|
||||||
|
}: ReportViolationParams) =>
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'ILLEGAL_MANIFEST_DEPENDENCY',
|
||||||
|
data: {
|
||||||
|
violations: [
|
||||||
|
...offendingDependencies.map(
|
||||||
|
({ id, pluginId, group, visibility }) =>
|
||||||
|
`⚠ Illegal dependency on manifest: Plugin "${currentPlugin.pluginId}" (package: "${currentPlugin.id}"; group: "${currentPlugin.group}") depends on "${pluginId}" (package: "${id}"; group: ${group}/${visibility}). File: ${manifestPath}`
|
||||||
|
),
|
||||||
|
].join('\n'),
|
||||||
|
suggestion: formatSuggestions([
|
||||||
|
`Please review the dependencies in your plugin's manifest (kibana.jsonc).`,
|
||||||
|
`Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
|
||||||
|
`Address the conflicting dependencies by refactoring the code`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,6 +14,7 @@
|
||||||
"@kbn/import-resolver",
|
"@kbn/import-resolver",
|
||||||
"@kbn/repo-source-classifier",
|
"@kbn/repo-source-classifier",
|
||||||
"@kbn/repo-info",
|
"@kbn/repo-info",
|
||||||
|
"@kbn/repo-packages",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -63,7 +63,11 @@ export const CodeownersCommand: GenerateCommand = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCodeowners = `${GENERATED_START}${pkgs
|
const newCodeowners = `${GENERATED_START}${pkgs
|
||||||
.map((pkg) => `${pkg.normalizedRepoRelativeDir} ${pkg.manifest.owner.join(' ')}`)
|
.map(
|
||||||
|
(pkg) =>
|
||||||
|
pkg.normalizedRepoRelativeDir +
|
||||||
|
(pkg.manifest.owner.length ? ' ' + pkg.manifest.owner.join(' ') : '')
|
||||||
|
)
|
||||||
.join('\n')}${GENERATED_END}${content}${ULTIMATE_PRIORITY_RULES}`;
|
.join('\n')}${GENERATED_END}${content}${ULTIMATE_PRIORITY_RULES}`;
|
||||||
|
|
||||||
if (newCodeowners === oldCodeowners) {
|
if (newCodeowners === oldCodeowners) {
|
||||||
|
|
|
@ -48,6 +48,20 @@ export const MANIFEST_V2: JSONSchema = {
|
||||||
For additional codeowners, the value can be an array of user/team names.
|
For additional codeowners, the value can be an array of user/team names.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
group: {
|
||||||
|
enum: ['common', 'platform', 'observability', 'security', 'search'],
|
||||||
|
description: desc`
|
||||||
|
Specifies the group to which this module pertains.
|
||||||
|
`,
|
||||||
|
default: 'common',
|
||||||
|
},
|
||||||
|
visibility: {
|
||||||
|
enum: ['private', 'shared'],
|
||||||
|
description: desc`
|
||||||
|
Specifies the visibility of this module, i.e. whether it can be accessed by everybody or only modules in the same group
|
||||||
|
`,
|
||||||
|
default: 'shared',
|
||||||
|
},
|
||||||
devOnly: {
|
devOnly: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: desc`
|
description: desc`
|
||||||
|
|
30
packages/kbn-manifest/README.md
Normal file
30
packages/kbn-manifest/README.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# @kbn/manifest
|
||||||
|
|
||||||
|
This package contains a CLI to list `kibana.jsonc` manifests and also to mass update their properties.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To list all `kibana.jsonc` manifests, run the following command from the root of the Kibana repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/manifest --list all
|
||||||
|
```
|
||||||
|
|
||||||
|
To print a manifest by packageId or by pluginId, run the following command from the root of the Kibana repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/manifest --package @kbn/package_name
|
||||||
|
node scripts/manifest --plugin pluginId
|
||||||
|
```
|
||||||
|
|
||||||
|
To update properties in one or more manifest files, run the following command from the root of the Kibana repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/manifest \
|
||||||
|
--package @kbn/package_1 \
|
||||||
|
--package @kbn/package_2 \
|
||||||
|
# ...
|
||||||
|
--package @kbn/package_N \
|
||||||
|
--set path.to.property1=value \
|
||||||
|
--set property2=value
|
||||||
|
```
|
46
packages/kbn-manifest/index.ts
Normal file
46
packages/kbn-manifest/index.ts
Normal file
|
@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { run } from '@kbn/dev-cli-runner';
|
||||||
|
import { listManifestFiles, printManifest, updateManifest } from './manifest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CLI to manipulate Kibana package manifest files
|
||||||
|
*/
|
||||||
|
export const runKbnManifestCli = () => {
|
||||||
|
run(
|
||||||
|
async ({ log, flags }) => {
|
||||||
|
if (flags.list === 'all') {
|
||||||
|
listManifestFiles(flags, log);
|
||||||
|
} else {
|
||||||
|
if (!flags.package && !flags.plugin) {
|
||||||
|
throw new Error('You must specify the identifer of the --package or --plugin to update.');
|
||||||
|
}
|
||||||
|
await updateManifest(flags, log);
|
||||||
|
await printManifest(flags, log);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
log: {
|
||||||
|
defaultLevel: 'info',
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
string: ['list', 'package', 'plugin', 'set', 'unset'],
|
||||||
|
help: `
|
||||||
|
Usage: node scripts/manifest --package <packageId> --set group=platform --set visibility=private
|
||||||
|
--list all List all the manifests
|
||||||
|
--package [packageId] Select a package to update.
|
||||||
|
--plugin [pluginId] Select a plugin to update.
|
||||||
|
--set [property] [value] Set the desired "[property]": "[value]"
|
||||||
|
--unset [property] Removes the desired "[property]: value" from the manifest
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
14
packages/kbn-manifest/jest.config.js
Normal file
14
packages/kbn-manifest/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: '@kbn/test/jest_node',
|
||||||
|
rootDir: '../..',
|
||||||
|
roots: ['<rootDir>/packages/kbn-manifest'],
|
||||||
|
};
|
5
packages/kbn-manifest/kibana.jsonc
Normal file
5
packages/kbn-manifest/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"type": "shared-server",
|
||||||
|
"id": "@kbn/manifest",
|
||||||
|
"owner": "@elastic/kibana-core"
|
||||||
|
}
|
113
packages/kbn-manifest/manifest.ts
Normal file
113
packages/kbn-manifest/manifest.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { writeFile } from 'fs/promises';
|
||||||
|
import { flatMap, unset } from 'lodash';
|
||||||
|
import { set } from '@kbn/safer-lodash-set';
|
||||||
|
import type { ToolingLog } from '@kbn/tooling-log';
|
||||||
|
import type { Flags } from '@kbn/dev-cli-runner';
|
||||||
|
import { type Package, getPackages } from '@kbn/repo-packages';
|
||||||
|
import { REPO_ROOT } from '@kbn/repo-info';
|
||||||
|
|
||||||
|
const MANIFEST_FILE = 'kibana.jsonc';
|
||||||
|
|
||||||
|
const getKibanaJsonc = (flags: Flags, log: ToolingLog): Package[] => {
|
||||||
|
const modules = getPackages(REPO_ROOT);
|
||||||
|
|
||||||
|
let packageIds: string[] = [];
|
||||||
|
let pluginIds: string[] = [];
|
||||||
|
|
||||||
|
if (typeof flags.package === 'string') {
|
||||||
|
packageIds = [flags.package].filter(Boolean);
|
||||||
|
} else if (Array.isArray(flags.package)) {
|
||||||
|
packageIds = [...flags.package].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof flags.plugin === 'string') {
|
||||||
|
pluginIds = [flags.plugin].filter(Boolean);
|
||||||
|
} else if (Array.isArray(flags.plugin)) {
|
||||||
|
pluginIds = [...flags.plugin].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules.filter(
|
||||||
|
(pkg) =>
|
||||||
|
packageIds.includes(pkg.id) || (pkg.isPlugin() && pluginIds.includes(pkg.manifest.plugin.id))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listManifestFiles = (flags: Flags, log: ToolingLog) => {
|
||||||
|
const modules = getPackages(REPO_ROOT);
|
||||||
|
modules
|
||||||
|
.filter((module) => module.manifest.type === 'plugin')
|
||||||
|
.forEach((module) => {
|
||||||
|
log.info(join(module.directory, MANIFEST_FILE), module.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printManifest = (flags: Flags, log: ToolingLog) => {
|
||||||
|
const kibanaJsoncs = getKibanaJsonc(flags, log);
|
||||||
|
kibanaJsoncs.forEach((kibanaJsonc) => {
|
||||||
|
const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
|
||||||
|
log.info('\n\nShowing manifest: ', manifestPath);
|
||||||
|
log.info(JSON.stringify(kibanaJsonc, null, 2));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateManifest = async (flags: Flags, log: ToolingLog) => {
|
||||||
|
let toSet: string[] = [];
|
||||||
|
let toUnset: string[] = [];
|
||||||
|
|
||||||
|
if (typeof flags.set === 'string') {
|
||||||
|
toSet = [flags.set].filter(Boolean);
|
||||||
|
} else if (Array.isArray(flags.set)) {
|
||||||
|
toSet = [...flags.set].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof flags.unset === 'string') {
|
||||||
|
toUnset = [flags.unset].filter(Boolean);
|
||||||
|
} else if (Array.isArray(flags.unset)) {
|
||||||
|
toUnset = [...flags.unset].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toSet.length && !toUnset.length) {
|
||||||
|
// no need to update anything
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kibanaJsoncs = getKibanaJsonc(flags, log);
|
||||||
|
|
||||||
|
for (let i = 0; i < kibanaJsoncs.length; ++i) {
|
||||||
|
const kibanaJsonc = kibanaJsoncs[i];
|
||||||
|
|
||||||
|
if (kibanaJsonc?.manifest) {
|
||||||
|
const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
|
||||||
|
log.info('Updating manifest: ', manifestPath);
|
||||||
|
toSet.forEach((propValue) => {
|
||||||
|
const [prop, value] = propValue.split('=');
|
||||||
|
log.info(`Setting "${prop}": "${value}"`);
|
||||||
|
set(kibanaJsonc.manifest, prop, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
toUnset.forEach((prop) => {
|
||||||
|
log.info(`Removing "${prop}"`);
|
||||||
|
unset(kibanaJsonc.manifest, prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
sanitiseManifest(kibanaJsonc);
|
||||||
|
|
||||||
|
await writeFile(manifestPath, JSON.stringify(kibanaJsonc.manifest, null, 2));
|
||||||
|
log.info('DONE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitiseManifest = (kibanaJsonc: Package) => {
|
||||||
|
kibanaJsonc.manifest.owner = flatMap(kibanaJsonc.manifest.owner.map((owner) => owner.split(' ')));
|
||||||
|
};
|
6
packages/kbn-manifest/package.json
Normal file
6
packages/kbn-manifest/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/manifest",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||||
|
}
|
23
packages/kbn-manifest/tsconfig.json
Normal file
23
packages/kbn-manifest/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types",
|
||||||
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/dev-cli-runner",
|
||||||
|
"@kbn/repo-info",
|
||||||
|
"@kbn/repo-packages",
|
||||||
|
"@kbn/safer-lodash-set",
|
||||||
|
"@kbn/tooling-log",
|
||||||
|
]
|
||||||
|
}
|
|
@ -7,6 +7,9 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type ModuleGroup = 'platform' | 'observability' | 'search' | 'security' | 'common';
|
||||||
|
export type ModuleVisibility = 'private' | 'shared';
|
||||||
|
|
||||||
export interface KibanaPackageJson {
|
export interface KibanaPackageJson {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
@ -27,4 +30,6 @@ export interface KibanaPackageJson {
|
||||||
[name: string]: string | undefined;
|
[name: string]: string | undefined;
|
||||||
};
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
|
group?: ModuleGroup;
|
||||||
|
visibility?: ModuleVisibility;
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,22 @@ class Package {
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
this.id = manifest.id;
|
this.id = manifest.id;
|
||||||
|
|
||||||
|
const { group, visibility } = this.determineGroupAndVisibility();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the group to which this package belongs
|
||||||
|
* @type {import('@kbn/repo-info/types').ModuleGroup}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.group = group;
|
||||||
|
/**
|
||||||
|
* the visibility of this package, i.e. whether it can be accessed by everybody or only modules in the same group
|
||||||
|
* @type {import('@kbn/repo-info/types').ModuleVisibility}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.visibility = visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,6 +156,24 @@ class Package {
|
||||||
return this.manifest.type === 'plugin';
|
return this.manifest.type === 'plugin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the group to which this package belongs
|
||||||
|
* @readonly
|
||||||
|
* @returns {import('@kbn/repo-info/types').ModuleGroup}
|
||||||
|
*/
|
||||||
|
getGroup() {
|
||||||
|
return this.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group
|
||||||
|
* @readonly
|
||||||
|
* @returns {import('@kbn/repo-info/types').ModuleVisibility}
|
||||||
|
*/
|
||||||
|
getVisibility() {
|
||||||
|
return this.visibility;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the package represents some type of plugin
|
* Returns true if the package represents some type of plugin
|
||||||
* @returns {import('./types').PluginCategoryInfo}
|
* @returns {import('./types').PluginCategoryInfo}
|
||||||
|
@ -158,6 +192,7 @@ class Package {
|
||||||
const oss = !dir.startsWith('x-pack/');
|
const oss = !dir.startsWith('x-pack/');
|
||||||
const example = dir.startsWith('examples/') || dir.startsWith('x-pack/examples/');
|
const example = dir.startsWith('examples/') || dir.startsWith('x-pack/examples/');
|
||||||
const testPlugin = dir.startsWith('test/') || dir.startsWith('x-pack/test/');
|
const testPlugin = dir.startsWith('test/') || dir.startsWith('x-pack/test/');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oss,
|
oss,
|
||||||
example,
|
example,
|
||||||
|
@ -165,6 +200,40 @@ class Package {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
determineGroupAndVisibility() {
|
||||||
|
const dir = this.normalizedRepoRelativeDir;
|
||||||
|
|
||||||
|
/** @type {import('@kbn/repo-info/types').ModuleGroup} */
|
||||||
|
let group = 'common';
|
||||||
|
/** @type {import('@kbn/repo-info/types').ModuleVisibility} */
|
||||||
|
let visibility = 'shared';
|
||||||
|
|
||||||
|
if (dir.startsWith('src/platform/') || dir.startsWith('x-pack/platform/')) {
|
||||||
|
group = 'platform';
|
||||||
|
visibility =
|
||||||
|
/src\/platform\/[^\/]+\/shared/.test(dir) || /x-pack\/platform\/[^\/]+\/shared/.test(dir)
|
||||||
|
? 'shared'
|
||||||
|
: 'private';
|
||||||
|
} else if (dir.startsWith('x-pack/solutions/search/')) {
|
||||||
|
group = 'search';
|
||||||
|
visibility = 'private';
|
||||||
|
} else if (dir.startsWith('x-pack/solutions/security/')) {
|
||||||
|
group = 'security';
|
||||||
|
visibility = 'private';
|
||||||
|
} else if (dir.startsWith('x-pack/solutions/observability/')) {
|
||||||
|
group = 'observability';
|
||||||
|
visibility = 'private';
|
||||||
|
} else {
|
||||||
|
group = this.manifest.group ?? 'common';
|
||||||
|
// if the group is 'private-only', enforce it
|
||||||
|
visibility = ['search', 'security', 'observability'].includes(group)
|
||||||
|
? 'private'
|
||||||
|
: this.manifest.visibility ?? 'shared';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { group, visibility };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom inspect handler so that logging variables in scripts/generate doesn't
|
* Custom inspect handler so that logging variables in scripts/generate doesn't
|
||||||
* print all the BUILD.bazel files
|
* print all the BUILD.bazel files
|
||||||
|
|
|
@ -225,16 +225,20 @@ function validatePackageManifest(parsed, repoRoot, path) {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
owner,
|
owner,
|
||||||
|
group,
|
||||||
|
visibility,
|
||||||
devOnly,
|
devOnly,
|
||||||
plugin,
|
|
||||||
sharedBrowserBundle,
|
|
||||||
build,
|
build,
|
||||||
description,
|
description,
|
||||||
serviceFolders,
|
serviceFolders,
|
||||||
...extra
|
...extra
|
||||||
} = parsed;
|
} = /** @type {import('./types').PackageManifestBaseFields} */ (/** @type {unknown} */ (parsed));
|
||||||
|
|
||||||
const extraKeys = Object.keys(extra);
|
const { plugin, sharedBrowserBundle } = parsed;
|
||||||
|
|
||||||
|
const extraKeys = Object.keys(extra).filter(
|
||||||
|
(key) => !['plugin', 'sharedBrowserBundle'].includes(key)
|
||||||
|
);
|
||||||
if (extraKeys.length) {
|
if (extraKeys.length) {
|
||||||
throw new Error(`unexpected keys in package manifest [${extraKeys.join(', ')}]`);
|
throw new Error(`unexpected keys in package manifest [${extraKeys.join(', ')}]`);
|
||||||
}
|
}
|
||||||
|
@ -258,6 +262,25 @@ function validatePackageManifest(parsed, repoRoot, path) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
group !== undefined &&
|
||||||
|
(!isSomeString(group) ||
|
||||||
|
!['platform', 'search', 'security', 'observability', 'common'].includes(group))
|
||||||
|
) {
|
||||||
|
throw err(
|
||||||
|
`plugin.group`,
|
||||||
|
group,
|
||||||
|
`must have a valid value ("platform" | "search" | "security" | "observability" | "common")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
visibility !== undefined &&
|
||||||
|
(!isSomeString(visibility) || !['private', 'shared'].includes(visibility))
|
||||||
|
) {
|
||||||
|
throw err(`plugin.visibility`, visibility, `must have a valid value ("private" | "shared")`);
|
||||||
|
}
|
||||||
|
|
||||||
if (devOnly !== undefined && typeof devOnly !== 'boolean') {
|
if (devOnly !== undefined && typeof devOnly !== 'boolean') {
|
||||||
throw err(`devOnly`, devOnly, `must be a boolean when defined`);
|
throw err(`devOnly`, devOnly, `must be a boolean when defined`);
|
||||||
}
|
}
|
||||||
|
@ -273,6 +296,8 @@ function validatePackageManifest(parsed, repoRoot, path) {
|
||||||
const base = {
|
const base = {
|
||||||
id,
|
id,
|
||||||
owner: Array.isArray(owner) ? owner : [owner],
|
owner: Array.isArray(owner) ? owner : [owner],
|
||||||
|
group,
|
||||||
|
visibility,
|
||||||
devOnly,
|
devOnly,
|
||||||
build: validatePackageManifestBuild(build),
|
build: validatePackageManifestBuild(build),
|
||||||
description,
|
description,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
import type { Package } from './package';
|
import type { Package } from './package';
|
||||||
import type { PLUGIN_CATEGORY } from './plugin_category_info';
|
import type { PLUGIN_CATEGORY } from './plugin_category_info';
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ export type KibanaPackageType =
|
||||||
| 'functional-tests'
|
| 'functional-tests'
|
||||||
| 'test-helper';
|
| 'test-helper';
|
||||||
|
|
||||||
interface PackageManifestBaseFields {
|
export interface PackageManifestBaseFields {
|
||||||
/**
|
/**
|
||||||
* The type of this package. Package types define how a package can and should
|
* The type of this package. Package types define how a package can and should
|
||||||
* be used/built. Some package types also change the way that packages are
|
* be used/built. Some package types also change the way that packages are
|
||||||
|
@ -91,6 +92,14 @@ interface PackageManifestBaseFields {
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
serviceFolders?: string[];
|
serviceFolders?: string[];
|
||||||
|
/**
|
||||||
|
* Specifies the group to which this package belongs
|
||||||
|
*/
|
||||||
|
group?: ModuleGroup;
|
||||||
|
/**
|
||||||
|
* Specifies the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group
|
||||||
|
*/
|
||||||
|
visibility?: ModuleVisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginPackageManifest extends PackageManifestBaseFields {
|
export interface PluginPackageManifest extends PackageManifestBaseFields {
|
||||||
|
|
|
@ -14,5 +14,8 @@
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/repo-info",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
63
packages/kbn-repo-source-classifier/src/group.ts
Normal file
63
packages/kbn-repo-source-classifier/src/group.ts
Normal file
|
@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
|
||||||
|
interface ModuleAttrs {
|
||||||
|
group: ModuleGroup;
|
||||||
|
visibility: ModuleVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MODULE_ATTRS: ModuleAttrs = {
|
||||||
|
group: 'common',
|
||||||
|
visibility: 'shared',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODULE_GROUPING_BY_PATH: Record<string, ModuleAttrs> = {
|
||||||
|
'src/platform/plugins/shared': {
|
||||||
|
group: 'platform',
|
||||||
|
visibility: 'shared',
|
||||||
|
},
|
||||||
|
'src/platform/plugins/internal': {
|
||||||
|
group: 'platform',
|
||||||
|
visibility: 'private',
|
||||||
|
},
|
||||||
|
'x-pack/platform/plugins/shared': {
|
||||||
|
group: 'platform',
|
||||||
|
visibility: 'shared',
|
||||||
|
},
|
||||||
|
'x-pack/platform/plugins/internal': {
|
||||||
|
group: 'platform',
|
||||||
|
visibility: 'private',
|
||||||
|
},
|
||||||
|
'x-pack/solutions/observability/plugins': {
|
||||||
|
group: 'observability',
|
||||||
|
visibility: 'private',
|
||||||
|
},
|
||||||
|
'x-pack/solutions/security/plugins': {
|
||||||
|
group: 'security',
|
||||||
|
visibility: 'private',
|
||||||
|
},
|
||||||
|
'x-pack/solutions/search/plugins': {
|
||||||
|
group: 'search',
|
||||||
|
visibility: 'private',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine a plugin's grouping information based on the path where it is defined
|
||||||
|
* @param packageRelativePath the path in the repo where the package is located
|
||||||
|
* @returns The grouping information that corresponds to the given path
|
||||||
|
*/
|
||||||
|
export function inferGroupAttrsFromPath(packageRelativePath: string): ModuleAttrs {
|
||||||
|
const grouping = Object.entries(MODULE_GROUPING_BY_PATH).find(([chunk]) =>
|
||||||
|
packageRelativePath.startsWith(chunk)
|
||||||
|
)?.[1];
|
||||||
|
return grouping ?? DEFAULT_MODULE_ATTRS;
|
||||||
|
}
|
|
@ -7,16 +7,24 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ModuleType } from './module_type';
|
import type { KibanaPackageManifest } from '@kbn/repo-packages';
|
||||||
import { PkgInfo } from './pkg_info';
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
|
import type { ModuleType } from './module_type';
|
||||||
|
import type { PkgInfo } from './pkg_info';
|
||||||
|
|
||||||
export interface ModuleId {
|
export interface ModuleId {
|
||||||
/** Type of the module */
|
/** Type of the module */
|
||||||
type: ModuleType;
|
type: ModuleType;
|
||||||
|
/** Specifies the group to which this module belongs */
|
||||||
|
group: ModuleGroup;
|
||||||
|
/** Specifies the module visibility, i.e. whether it can be accessed by everybody or only modules in the same group */
|
||||||
|
visibility: ModuleVisibility;
|
||||||
/** repo relative path to the module's source file */
|
/** repo relative path to the module's source file */
|
||||||
repoRel: string;
|
repoRel: string;
|
||||||
/** info about the package the source file is within, in the case the file is found within a package */
|
/** info about the package the source file is within, in the case the file is found within a package */
|
||||||
pkgInfo?: PkgInfo;
|
pkgInfo?: PkgInfo;
|
||||||
|
/** The type of package, as described in the manifest */
|
||||||
|
manifest?: KibanaPackageManifest;
|
||||||
/** path segments of the dirname of this */
|
/** path segments of the dirname of this */
|
||||||
dirs: string[];
|
dirs: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ImportResolver } from '@kbn/import-resolver';
|
import type { ImportResolver } from '@kbn/import-resolver';
|
||||||
import { ModuleId } from './module_id';
|
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||||
import { ModuleType } from './module_type';
|
import type { KibanaPackageManifest } from '@kbn/repo-packages/modern/types';
|
||||||
|
import type { ModuleId } from './module_id';
|
||||||
|
import type { ModuleType } from './module_type';
|
||||||
import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config';
|
import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config';
|
||||||
import { RepoPath } from './repo_path';
|
import { RepoPath } from './repo_path';
|
||||||
|
import { inferGroupAttrsFromPath } from './group';
|
||||||
|
|
||||||
const STATIC_EXTS = new Set(
|
const STATIC_EXTS = new Set(
|
||||||
'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl|xml'
|
'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl|xml'
|
||||||
|
@ -231,7 +234,43 @@ export class RepoSourceClassifier {
|
||||||
return 'common package';
|
return 'common package';
|
||||||
}
|
}
|
||||||
|
|
||||||
classify(absolute: string) {
|
private getManifest(path: RepoPath): KibanaPackageManifest | undefined {
|
||||||
|
const pkgInfo = path.getPkgInfo();
|
||||||
|
return pkgInfo?.pkgId ? this.resolver.getPkgManifest(pkgInfo!.pkgId) : undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Determine the "group" of a file
|
||||||
|
*/
|
||||||
|
private getGroup(path: RepoPath): ModuleGroup {
|
||||||
|
const attrs = inferGroupAttrsFromPath(path.getRepoRel());
|
||||||
|
const manifest = this.getManifest(path);
|
||||||
|
|
||||||
|
if (attrs.group !== 'common') {
|
||||||
|
// this package has been moved to a 'group-specific' folder, the group is determined by its location
|
||||||
|
return attrs.group;
|
||||||
|
} else {
|
||||||
|
// the package is still in its original location, allow Manifest to dictate its group
|
||||||
|
return manifest?.group ?? 'common';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the "visibility" of a file
|
||||||
|
*/
|
||||||
|
private getVisibility(path: RepoPath): ModuleVisibility {
|
||||||
|
const attrs = inferGroupAttrsFromPath(path.getRepoRel());
|
||||||
|
const manifest = this.getManifest(path);
|
||||||
|
|
||||||
|
if (attrs.group !== 'common') {
|
||||||
|
// this package has been moved to a 'group-specific' folder, the visibility is determined by its location
|
||||||
|
return attrs.visibility;
|
||||||
|
} else {
|
||||||
|
// the package is still in its original location, allow Manifest to dictate its visibility
|
||||||
|
return manifest?.visibility ?? 'shared';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
classify(absolute: string): ModuleId {
|
||||||
const path = this.getRepoPath(absolute);
|
const path = this.getRepoPath(absolute);
|
||||||
const cached = this.ids.get(path);
|
const cached = this.ids.get(path);
|
||||||
|
|
||||||
|
@ -241,8 +280,12 @@ export class RepoSourceClassifier {
|
||||||
|
|
||||||
const id: ModuleId = {
|
const id: ModuleId = {
|
||||||
type: this.getType(path),
|
type: this.getType(path),
|
||||||
|
group: this.getGroup(path),
|
||||||
|
visibility: this.getVisibility(path),
|
||||||
repoRel: path.getRepoRel(),
|
repoRel: path.getRepoRel(),
|
||||||
pkgInfo: path.getPkgInfo() ?? undefined,
|
pkgInfo: path.getPkgInfo() ?? undefined,
|
||||||
|
manifest:
|
||||||
|
(path.getPkgInfo() && this.resolver.getPkgManifest(path.getPkgInfo()!.pkgId)) ?? undefined,
|
||||||
dirs: path.getSegs(),
|
dirs: path.getSegs(),
|
||||||
};
|
};
|
||||||
this.ids.set(path, id);
|
this.ids.set(path, id);
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/import-resolver",
|
"@kbn/import-resolver",
|
||||||
"@kbn/repo-info",
|
"@kbn/repo-info",
|
||||||
|
"@kbn/repo-packages",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
11
scripts/manifest.js
Normal file
11
scripts/manifest.js
Normal file
|
@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('../src/setup_node_env');
|
||||||
|
require('@kbn/manifest').runKbnManifestCli();
|
|
@ -1188,6 +1188,8 @@
|
||||||
"@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"],
|
"@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"],
|
||||||
"@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"],
|
"@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"],
|
||||||
"@kbn/management-test-plugin/*": ["test/plugin_functional/plugins/management_test_plugin/*"],
|
"@kbn/management-test-plugin/*": ["test/plugin_functional/plugins/management_test_plugin/*"],
|
||||||
|
"@kbn/manifest": ["packages/kbn-manifest"],
|
||||||
|
"@kbn/manifest/*": ["packages/kbn-manifest/*"],
|
||||||
"@kbn/mapbox-gl": ["packages/kbn-mapbox-gl"],
|
"@kbn/mapbox-gl": ["packages/kbn-mapbox-gl"],
|
||||||
"@kbn/mapbox-gl/*": ["packages/kbn-mapbox-gl/*"],
|
"@kbn/mapbox-gl/*": ["packages/kbn-mapbox-gl/*"],
|
||||||
"@kbn/maps-custom-raster-source-plugin": ["x-pack/examples/third_party_maps_source_example"],
|
"@kbn/maps-custom-raster-source-plugin": ["x-pack/examples/third_party_maps_source_example"],
|
||||||
|
|
|
@ -5671,6 +5671,10 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/manifest@link:packages/kbn-manifest":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@kbn/mapbox-gl@link:packages/kbn-mapbox-gl":
|
"@kbn/mapbox-gl@link:packages/kbn-mapbox-gl":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue