Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Kibana is organized into modules, each defined by a `kibana.jsonc`: core, packages, and plugin packages. Aside from tooling and testing, most code lives in these modules.
- Packages are reusable units with explicit boundaries and a single public entry point (no subpath imports), usually with a focused purpose.
- Plugins are a package type (`type: "plugin"`) that include a plugin class with setup/start/stop lifecycles, utilized by the core platform to enable applications.
- **Server plugin entry (`server/index.ts`)** should not load `./plugin` until the plugin may run. Use `import type` (and `export type`) for types from `./plugin`, keep shared config in `config.ts` / `../common/config` (not re-exported runtime values from `./plugin` at the entry), and instantiate the implementation with `await import('./plugin')` inside the async `plugin` initializer. Static value imports, `export { … }` / `export *` of values, `import './plugin'`, and `require('./plugin')` in that entry force Node to parse and execute `plugin.ts` even when the plugin is disabled. `@kbn/eslint/no_sync_import_from_plugin` in `@kbn/eslint-config` enforces this on plugin `server/index.ts` files (see [PR #170856](https://github.com/elastic/kibana/pull/170856) and [issue #171080](https://github.com/elastic/kibana/issues/171080)).
- Plugins that depend on other plugins rely on the contracts returned by those lifecycles, so circular dependencies must be avoided.
- Module IDs (typically `@kbn/...`) live in `kibana.jsonc`; `package.json` names are derived where present.
- Plugin IDs are additional camelCase IDs under `plugin.id` in `kibana.jsonc`, used by core platform and other plugins.
Expand Down Expand Up @@ -57,6 +58,7 @@ Follow existing patterns in the target area first; below are common defaults.
### Linting
`node scripts/eslint --fix $(git diff --name-only)`
- Never suppress linting errors with `eslint-disable`; fix the root cause.
- Plugin `server/index.ts` files are checked by `@kbn/eslint/no_sync_import_from_plugin` (see plugin server entry note above).

### Formatting
- Follow existing formatting in the file; do not reformat unrelated code.
Expand Down
23 changes: 23 additions & 0 deletions packages/kbn-eslint-config/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,27 @@ module.exports = {
'@elastic/eui/prefer-eui-icon-tip': 'error',
'@elastic/eui/sr-output-disabled-tooltip': 'error',
},

overrides: [
{
files: [
'src/platform/plugins/**/server/index.ts',
'x-pack/platform/plugins/**/server/index.ts',
'x-pack/solutions/**/plugins/**/server/index.ts',
'examples/**/server/index.ts',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples/**/server/index.ts glob only matches the repo-root examples/ tree, so the 5 plugin entries under x-pack/examples/ (e.g. alerting_example, screenshotting_example, triggers_actions_ui_example, gen_ai_streaming_response_example, third_party_vis_lens_example) are not enforced. If example plugins are intended to follow the same pattern, consider also covering x-pack/examples/.

Suggested change
'examples/**/server/index.ts',
'examples/**/server/index.ts',
'x-pack/examples/**/server/index.ts',

'packages/kbn-mock-idp-plugin/server/index.ts',
],
excludedFiles: ['**/test/**'],
rules: {
/**
* Plugin server entry should not load ./plugin until the plugin is enabled.
* @see https://github.com/elastic/kibana/pull/170856
* @see https://github.com/elastic/kibana/issues/171080
*
* Enforced in CI; violation count should fall as lazy-load `server/index.ts` migrations land.
*/
'@kbn/eslint/no_sync_import_from_plugin': 'error',
},
},
],
};
1 change: 1 addition & 0 deletions packages/kbn-eslint-plugin-eslint/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ module.exports = {
require_kbn_fs: require('./rules/require_kbn_fs'),
require_include_in_check_a11y: require('./rules/require_include_in_check_a11y'),
no_wrapped_error_in_logger: require('./rules/no_wrapped_error_in_logger'),
no_sync_import_from_plugin: require('./rules/no_sync_import_from_plugin'),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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".
*/

/** @typedef {import("eslint").Rule.RuleModule} Rule */

const MESSAGE =
"Do not statically import or re-export values from './plugin' in the server plugin entry. Use `import type` for types only, move shared config to `config.ts` when needed, and load the implementation with `await import('./plugin')` so disabled plugins do not load this module. See https://github.com/elastic/kibana/pull/170856.";

/**
* @param {string | undefined} source
* @returns {boolean}
*/
function isPluginModuleSpecifier(source) {
return source === './plugin' || source === '.\\plugin';
}

/** @type {Rule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Disallow static value imports and value re-exports from `./plugin` in plugin server entry files.',
},
schema: [],
messages: {
noSyncImportFromPlugin: MESSAGE,
},
},

create(context) {
return {
ImportDeclaration(node) {
if (!node.source || !isPluginModuleSpecifier(node.source.value)) {
return;
}

if (node.importKind === 'type') {
return;
}

if (node.specifiers.length === 0) {
context.report({ node, messageId: 'noSyncImportFromPlugin' });
return;
}

for (const spec of node.specifiers) {
if (spec.type === 'ImportSpecifier' && spec.importKind === 'type') {
continue;
}
context.report({ node: spec, messageId: 'noSyncImportFromPlugin' });
return;
}
},

ExportNamedDeclaration(node) {
if (!node.source || !isPluginModuleSpecifier(node.source.value)) {
return;
}

if (node.exportKind === 'type') {
return;
}

for (const spec of node.specifiers) {
if (spec.type === 'ExportSpecifier' && spec.exportKind === 'type') {
continue;
}
context.report({ node: spec, messageId: 'noSyncImportFromPlugin' });
return;
}
},

ExportAllDeclaration(node) {
if (!node.source || !isPluginModuleSpecifier(node.source.value)) {
return;
}

if (node.exportKind === 'type') {
return;
}

context.report({ node, messageId: 'noSyncImportFromPlugin' });
},

CallExpression(node) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments[0]?.type === 'Literal' &&
typeof node.arguments[0].value === 'string' &&
isPluginModuleSpecifier(node.arguments[0].value)
) {
context.report({ node, messageId: 'noSyncImportFromPlugin' });
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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".
*/

const { RuleTester } = require('eslint');
const rule = require('./no_sync_import_from_plugin');
const dedent = require('dedent');

const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
ecmaFeatures: {
jsx: true,
},
},
});

const ERR = { messageId: 'noSyncImportFromPlugin' };

ruleTester.run('@kbn/eslint/no_sync_import_from_plugin', rule, {
valid: [
{
code: dedent`
import type { PluginInitializerContext } from '@kbn/core/server';
import type { MyPluginSetup } from './plugin';
export const plugin = async (ctx: PluginInitializerContext) => {
const { MyPlugin } = await import('./plugin');
return new MyPlugin(ctx);
};
`,
},
{
code: dedent`
import { config } from './config';
export const plugin = async () => {
const { Plugin } = await import('./plugin');
return new Plugin();
};
export { config };
`,
},
{
code: dedent`
export type { FooSetup } from './plugin';
`,
},
{
code: dedent`
import type { Foo } from './other';
`,
},
{
code: dedent`
const m = await import('./plugin');
`,
},
],

invalid: [
{
code: `import { FooPlugin } from './plugin';`,
errors: [ERR],
},
{
code: dedent`
import type { Setup } from './plugin';
import { FooPlugin } from './plugin';
`,
errors: [ERR],
},
{
code: `import './plugin';`,
errors: [ERR],
},
{
code: `export { FooPlugin } from './plugin';`,
errors: [ERR],
},
{
code: `export * from './plugin';`,
errors: [ERR],
},
{
code: `require('./plugin');`,
errors: [ERR],
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
AgentContextLayerSetupDependencies,
AgentContextLayerStartDependencies,
} from './types';
import { AgentContextLayerPlugin } from './plugin';

export type {
AgentContextLayerPluginSetup,
Expand Down Expand Up @@ -41,5 +40,6 @@ export const plugin: PluginInitializer<
AgentContextLayerSetupDependencies,
AgentContextLayerStartDependencies
> = async (pluginInitializerContext: PluginInitializerContext) => {
const { AgentContextLayerPlugin } = await import('./plugin');
return new AgentContextLayerPlugin(pluginInitializerContext);
};
7 changes: 4 additions & 3 deletions x-pack/platform/plugins/shared/inbox/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
InboxSetupDependencies,
InboxStartDependencies,
} from './types';
import { InboxPlugin } from './plugin';

export type { InboxPluginSetup, InboxPluginStart };

Expand All @@ -22,7 +21,9 @@ export const plugin: PluginInitializer<
InboxPluginStart,
InboxSetupDependencies,
InboxStartDependencies
> = async (pluginInitializerContext: PluginInitializerContext<InboxConfig>) =>
new InboxPlugin(pluginInitializerContext);
> = async (pluginInitializerContext: PluginInitializerContext<InboxConfig>) => {
const { InboxPlugin } = await import('./plugin');
return new InboxPlugin(pluginInitializerContext);
};

export { config } from './config';
Loading