mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [Add ESLINT constraints to detect inter-group dependencies (#194810)](https://github.com/elastic/kibana/pull/194810) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Gerard Soldevila","email":"gerard.soldevila@elastic.co"},"sourceCommit":{"committedDate":"2024-10-22T11:34:19Z","message":"Add ESLINT constraints to detect inter-group dependencies (#194810)\n\n## Summary\r\n\r\nAddresses https://github.com/elastic/kibana-team/issues/1175\r\n\r\nAs part of the **Sustainable Kibana Architecture** initiative, this PR\r\nsets the foundation to start classifying plugins in isolated groups,\r\nmatching our current solutions / project types:\r\n\r\n* It adds support for the following fields in the packages' manifests\r\n(kibana.jsonc):\r\n* `group?: 'search' | 'security' | 'observability' | 'platform' |\r\n'common'`\r\n * `visibility?: 'private' | 'shared'`\r\n\r\n* It proposes a folder structure to automatically infer groups:\r\n```javascript\r\n 'src/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'src/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'x-pack/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/observability/plugins': {\r\n group: 'observability',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/security/plugins': {\r\n group: 'security',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/search/plugins': {\r\n group: 'search',\r\n visibility: 'private',\r\n },\r\n```\r\n\r\n* If a plugin is moved to one of the specific locations above, the group\r\nand visibility in the manifest (if specified) must match those inferred\r\nfrom the path.\r\n* Plugins that are not relocated are considered: `group: 'common',\r\nvisibility: 'shared'` by default. As soon as we specify a custom\r\n`group`, the ESLINT rules will check violations against dependencies /\r\ndependants.\r\n\r\nThe ESLINT rules are pretty simple:\r\n* Plugins can only depend on:\r\n * Plugins in the same group\r\n * OR plugins with `'shared'` visibility\r\n* Plugins in `'observability', 'security', 'search'` groups are\r\nmandatorily `'private'`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"2a085e103afe8c7bdfb626d0dc683fc8be0e6c05","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","backport missing","v9.0.0","release_note:feature","backport:prev-minor"],"number":194810,"url":"https://github.com/elastic/kibana/pull/194810","mergeCommit":{"message":"Add ESLINT constraints to detect inter-group dependencies (#194810)\n\n## Summary\r\n\r\nAddresses https://github.com/elastic/kibana-team/issues/1175\r\n\r\nAs part of the **Sustainable Kibana Architecture** initiative, this PR\r\nsets the foundation to start classifying plugins in isolated groups,\r\nmatching our current solutions / project types:\r\n\r\n* It adds support for the following fields in the packages' manifests\r\n(kibana.jsonc):\r\n* `group?: 'search' | 'security' | 'observability' | 'platform' |\r\n'common'`\r\n * `visibility?: 'private' | 'shared'`\r\n\r\n* It proposes a folder structure to automatically infer groups:\r\n```javascript\r\n 'src/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'src/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'x-pack/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/observability/plugins': {\r\n group: 'observability',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/security/plugins': {\r\n group: 'security',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/search/plugins': {\r\n group: 'search',\r\n visibility: 'private',\r\n },\r\n```\r\n\r\n* If a plugin is moved to one of the specific locations above, the group\r\nand visibility in the manifest (if specified) must match those inferred\r\nfrom the path.\r\n* Plugins that are not relocated are considered: `group: 'common',\r\nvisibility: 'shared'` by default. As soon as we specify a custom\r\n`group`, the ESLINT rules will check violations against dependencies /\r\ndependants.\r\n\r\nThe ESLINT rules are pretty simple:\r\n* Plugins can only depend on:\r\n * Plugins in the same group\r\n * OR plugins with `'shared'` visibility\r\n* Plugins in `'observability', 'security', 'search'` groups are\r\nmandatorily `'private'`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"2a085e103afe8c7bdfb626d0dc683fc8be0e6c05"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194810","number":194810,"mergeCommit":{"message":"Add ESLINT constraints to detect inter-group dependencies (#194810)\n\n## Summary\r\n\r\nAddresses https://github.com/elastic/kibana-team/issues/1175\r\n\r\nAs part of the **Sustainable Kibana Architecture** initiative, this PR\r\nsets the foundation to start classifying plugins in isolated groups,\r\nmatching our current solutions / project types:\r\n\r\n* It adds support for the following fields in the packages' manifests\r\n(kibana.jsonc):\r\n* `group?: 'search' | 'security' | 'observability' | 'platform' |\r\n'common'`\r\n * `visibility?: 'private' | 'shared'`\r\n\r\n* It proposes a folder structure to automatically infer groups:\r\n```javascript\r\n 'src/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'src/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/platform/plugins/shared': {\r\n group: 'platform',\r\n visibility: 'shared',\r\n },\r\n 'x-pack/platform/plugins/internal': {\r\n group: 'platform',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/observability/plugins': {\r\n group: 'observability',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/security/plugins': {\r\n group: 'security',\r\n visibility: 'private',\r\n },\r\n 'x-pack/solutions/search/plugins': {\r\n group: 'search',\r\n visibility: 'private',\r\n },\r\n```\r\n\r\n* If a plugin is moved to one of the specific locations above, the group\r\nand visibility in the manifest (if specified) must match those inferred\r\nfrom the path.\r\n* Plugins that are not relocated are considered: `group: 'common',\r\nvisibility: 'shared'` by default. As soon as we specify a custom\r\n`group`, the ESLINT rules will check violations against dependencies /\r\ndependants.\r\n\r\nThe ESLINT rules are pretty simple:\r\n* Plugins can only depend on:\r\n * Plugins in the same group\r\n * OR plugins with `'shared'` visibility\r\n* Plugins in `'observability', 'security', 'search'` groups are\r\nmandatorily `'private'`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"2a085e103afe8c7bdfb626d0dc683fc8be0e6c05"}}]}] BACKPORT-->
This commit is contained in:
parent
9d54a62014
commit
7b820130ab
36 changed files with 1277 additions and 48 deletions
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
|
@ -596,6 +596,7 @@ packages/kbn-management/settings/types @elastic/kibana-management
|
|||
packages/kbn-management/settings/utilities @elastic/kibana-management
|
||||
packages/kbn-management/storybook/config @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-gis
|
||||
x-pack/examples/third_party_maps_source_example @elastic/kibana-gis
|
||||
src/plugins/maps_ems @elastic/kibana-gis
|
||||
|
@ -930,9 +931,9 @@ packages/kbn-test-eui-helpers @elastic/kibana-visualizations
|
|||
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-subj-selector @elastic/kibana-operations @elastic/appex-qa
|
||||
x-pack/test_serverless
|
||||
test
|
||||
x-pack/test
|
||||
x-pack/test_serverless
|
||||
test
|
||||
x-pack/test
|
||||
x-pack/performance @elastic/appex-qa
|
||||
x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations
|
||||
x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations
|
||||
|
|
|
@ -633,6 +633,7 @@
|
|||
"@kbn/management-settings-types": "link:packages/kbn-management/settings/types",
|
||||
"@kbn/management-settings-utilities": "link:packages/kbn-management/settings/utilities",
|
||||
"@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/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example",
|
||||
"@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() {
|
||||
run(async ({ log }) => {
|
||||
log.info('looking for oss plugins');
|
||||
const ossPlugins = discoverPlugins('src/plugins');
|
||||
log.success(`found ${ossPlugins.length} plugins`);
|
||||
const ossLegacyPlugins = discoverPlugins('src/plugins');
|
||||
const ossPlatformPlugins = discoverPlugins('src/platform/plugins');
|
||||
log.success(`found ${ossLegacyPlugins.length + ossPlatformPlugins.length} plugins`);
|
||||
|
||||
log.info('looking for x-pack plugins');
|
||||
const xpackPlugins = discoverPlugins('x-pack/plugins');
|
||||
log.success(`found ${xpackPlugins.length} plugins`);
|
||||
const xpackLegacyPlugins = discoverPlugins('x-pack/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);
|
||||
Fs.writeFileSync(OUTPUT_PATH, generatePluginList(ossPlugins, xpackPlugins));
|
||||
Fs.writeFileSync(
|
||||
OUTPUT_PATH,
|
||||
generatePluginList(
|
||||
[...ossLegacyPlugins, ...ossPlatformPlugins],
|
||||
[
|
||||
...xpackLegacyPlugins,
|
||||
...xpackPlatformPlugins,
|
||||
...xpackSearchPlugins,
|
||||
...xpackSecurityPlugins,
|
||||
...xpackObservabilityPlugins,
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -326,7 +326,8 @@ module.exports = {
|
|||
'@kbn/imports/uniform_imports': 'error',
|
||||
'@kbn/imports/no_unused_imports': 'error',
|
||||
'@kbn/imports/no_boundary_crossing': 'error',
|
||||
|
||||
'@kbn/imports/no_group_crossing_manifests': 'error',
|
||||
'@kbn/imports/no_group_crossing_imports': 'error',
|
||||
'no-new-func': 'error',
|
||||
'no-implied-eval': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
|
|
|
@ -12,4 +12,6 @@ export const PROTECTED_RULES = new Set([
|
|||
'@kbn/disable/no_protected_eslint_disable',
|
||||
'@kbn/disable/no_naked_eslint_disable',
|
||||
'@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 { NoUnusedImportsRule } from './src/rules/no_unused_imports';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
@ -25,5 +27,7 @@ export const rules = {
|
|||
exports_moved_packages: ExportsMovedPackagesRule,
|
||||
no_unused_imports: NoUnusedImportsRule,
|
||||
no_boundary_crossing: NoBoundaryCrossingRule,
|
||||
no_group_crossing_imports: NoGroupCrossingImportsRule,
|
||||
no_group_crossing_manifests: NoGroupCrossingManifestsRule,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 { 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 { formatSuggestions } from '../helpers/report';
|
||||
|
||||
const make = (from: ModuleType, to: ModuleType, imp = 'import') => ({
|
||||
filename: `${from}.ts`,
|
||||
|
@ -107,13 +108,12 @@ for (const [name, tester] of [tsTester, babelTester]) {
|
|||
data: {
|
||||
importedType: 'server package',
|
||||
ownType: 'common package',
|
||||
suggestion: ` ${dedent`
|
||||
Suggestions:
|
||||
- Remove the import statement.
|
||||
- Limit your imports to "common package" or "static" code.
|
||||
- Covert to a type-only import.
|
||||
- Reach out to #kibana-operations for help.
|
||||
`}`,
|
||||
suggestion: formatSuggestions([
|
||||
'Remove the import statement.',
|
||||
'Limit your imports to "common package" or "static" code.',
|
||||
'Covert to a type-only import.',
|
||||
'Reach out to #kibana-operations for help.',
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,13 +12,14 @@ import Path from 'path';
|
|||
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
import * as Bt from '@babel/types';
|
||||
import type { Rule } from 'eslint';
|
||||
import ESTree from 'estree';
|
||||
import { ModuleType } from '@kbn/repo-source-classifier';
|
||||
import type { Node } from 'estree';
|
||||
import type { ModuleType } from '@kbn/repo-source-classifier';
|
||||
|
||||
import { visitAllImportStatements, Importer } 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, toList } from '../helpers/report';
|
||||
|
||||
const ANY = Symbol();
|
||||
|
||||
|
@ -33,22 +34,6 @@ const IMPORTABLE_FROM: Record<ModuleType, ModuleType[] | typeof 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) => {
|
||||
// handle babel nodes
|
||||
if (Bt.isImportDeclaration(importer)) {
|
||||
|
@ -125,7 +110,7 @@ export const NoBoundaryCrossingRule: Rule.RuleModule = {
|
|||
|
||||
if (!importable.includes(imported.type)) {
|
||||
context.report({
|
||||
node: node as ESTree.Node,
|
||||
node: node as Node,
|
||||
messageId: 'TYPE_MISMATCH',
|
||||
data: {
|
||||
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/repo-source-classifier",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/repo-packages",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -63,7 +63,11 @@ export const CodeownersCommand: GenerateCommand = {
|
|||
}
|
||||
|
||||
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}`;
|
||||
|
||||
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.
|
||||
`,
|
||||
},
|
||||
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: {
|
||||
type: 'boolean',
|
||||
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".
|
||||
*/
|
||||
|
||||
export type ModuleGroup = 'platform' | 'observability' | 'search' | 'security' | 'common';
|
||||
export type ModuleVisibility = 'private' | 'shared';
|
||||
|
||||
export interface KibanaPackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
|
@ -27,4 +30,6 @@ export interface KibanaPackageJson {
|
|||
[name: string]: string | undefined;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
group?: ModuleGroup;
|
||||
visibility?: ModuleVisibility;
|
||||
}
|
||||
|
|
|
@ -116,6 +116,22 @@ class Package {
|
|||
* @readonly
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {import('./types').PluginCategoryInfo}
|
||||
|
@ -158,6 +192,7 @@ class Package {
|
|||
const oss = !dir.startsWith('x-pack/');
|
||||
const example = dir.startsWith('examples/') || dir.startsWith('x-pack/examples/');
|
||||
const testPlugin = dir.startsWith('test/') || dir.startsWith('x-pack/test/');
|
||||
|
||||
return {
|
||||
oss,
|
||||
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
|
||||
* print all the BUILD.bazel files
|
||||
|
|
|
@ -225,16 +225,20 @@ function validatePackageManifest(parsed, repoRoot, path) {
|
|||
type,
|
||||
id,
|
||||
owner,
|
||||
group,
|
||||
visibility,
|
||||
devOnly,
|
||||
plugin,
|
||||
sharedBrowserBundle,
|
||||
build,
|
||||
description,
|
||||
serviceFolders,
|
||||
...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) {
|
||||
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') {
|
||||
throw err(`devOnly`, devOnly, `must be a boolean when defined`);
|
||||
}
|
||||
|
@ -273,6 +296,8 @@ function validatePackageManifest(parsed, repoRoot, path) {
|
|||
const base = {
|
||||
id,
|
||||
owner: Array.isArray(owner) ? owner : [owner],
|
||||
group,
|
||||
visibility,
|
||||
devOnly,
|
||||
build: validatePackageManifestBuild(build),
|
||||
description,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* 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 { PLUGIN_CATEGORY } from './plugin_category_info';
|
||||
|
||||
|
@ -44,7 +45,7 @@ export type KibanaPackageType =
|
|||
| 'functional-tests'
|
||||
| 'test-helper';
|
||||
|
||||
interface PackageManifestBaseFields {
|
||||
export interface PackageManifestBaseFields {
|
||||
/**
|
||||
* 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
|
||||
|
@ -91,6 +92,14 @@ interface PackageManifestBaseFields {
|
|||
* @deprecated
|
||||
*/
|
||||
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 {
|
||||
|
|
|
@ -14,5 +14,8 @@
|
|||
],
|
||||
"exclude": [
|
||||
"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".
|
||||
*/
|
||||
|
||||
import { ModuleType } from './module_type';
|
||||
import { PkgInfo } from './pkg_info';
|
||||
import type { KibanaPackageManifest } from '@kbn/repo-packages';
|
||||
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||
import type { ModuleType } from './module_type';
|
||||
import type { PkgInfo } from './pkg_info';
|
||||
|
||||
export interface ModuleId {
|
||||
/** Type of the module */
|
||||
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 */
|
||||
repoRel: string;
|
||||
/** info about the package the source file is within, in the case the file is found within a package */
|
||||
pkgInfo?: PkgInfo;
|
||||
/** The type of package, as described in the manifest */
|
||||
manifest?: KibanaPackageManifest;
|
||||
/** path segments of the dirname of this */
|
||||
dirs: string[];
|
||||
}
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ImportResolver } from '@kbn/import-resolver';
|
||||
import { ModuleId } from './module_id';
|
||||
import { ModuleType } from './module_type';
|
||||
import type { ImportResolver } from '@kbn/import-resolver';
|
||||
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
|
||||
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 { RepoPath } from './repo_path';
|
||||
import { inferGroupAttrsFromPath } from './group';
|
||||
|
||||
const STATIC_EXTS = new Set(
|
||||
'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';
|
||||
}
|
||||
|
||||
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 cached = this.ids.get(path);
|
||||
|
||||
|
@ -241,8 +280,12 @@ export class RepoSourceClassifier {
|
|||
|
||||
const id: ModuleId = {
|
||||
type: this.getType(path),
|
||||
group: this.getGroup(path),
|
||||
visibility: this.getVisibility(path),
|
||||
repoRel: path.getRepoRel(),
|
||||
pkgInfo: path.getPkgInfo() ?? undefined,
|
||||
manifest:
|
||||
(path.getPkgInfo() && this.resolver.getPkgManifest(path.getPkgInfo()!.pkgId)) ?? undefined,
|
||||
dirs: path.getSegs(),
|
||||
};
|
||||
this.ids.set(path, id);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"kbn_references": [
|
||||
"@kbn/import-resolver",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/repo-packages",
|
||||
],
|
||||
"exclude": [
|
||||
"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();
|
|
@ -1186,6 +1186,8 @@
|
|||
"@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/manifest": ["packages/kbn-manifest"],
|
||||
"@kbn/manifest/*": ["packages/kbn-manifest/*"],
|
||||
"@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"],
|
||||
|
|
|
@ -5637,6 +5637,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/manifest@link:packages/kbn-manifest":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/mapbox-gl@link:packages/kbn-mapbox-gl":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue