diff --git a/packages/kbn-docs-utils/index.ts b/packages/kbn-docs-utils/index.ts index 31283b46d65a6..f206c31191019 100644 --- a/packages/kbn-docs-utils/index.ts +++ b/packages/kbn-docs-utils/index.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { runBuildApiDocsCli } from './src'; +export { runBuildApiDocsCli, runCheckPackageDocsCli } from './src'; export { findPlugins, findTeamPlugins } from './src/find_plugins'; diff --git a/packages/kbn-docs-utils/jest.config.js b/packages/kbn-docs-utils/jest.config.js index 54dd1bb144d46..ef90e642c5bd6 100644 --- a/packages/kbn-docs-utils/jest.config.js +++ b/packages/kbn-docs-utils/jest.config.js @@ -18,4 +18,5 @@ module.exports = { '!/packages/kbn-docs-utils/src/**/*.test.ts', '!/packages/kbn-docs-utils/src/**/__fixtures__/**', ], + coveragePathIgnorePatterns: ['/packages/kbn-docs-utils/src/integration_tests/'], }; diff --git a/packages/kbn-docs-utils/src/README.md b/packages/kbn-docs-utils/src/README.md index 7710aa550b880..41906ef8fa16a 100644 --- a/packages/kbn-docs-utils/src/README.md +++ b/packages/kbn-docs-utils/src/README.md @@ -10,3 +10,16 @@ To generate the docs run ``` node scripts/build_api_docs ``` + +To validate documentation without writing files, run + +``` +node scripts/check_package_docs --plugin +``` + +You can use `--plugin` to filter by plugin id and `--package` to filter by package id (manifest.id). These filters can be used independently or together. Validation flags: + +- `--check ` (optional, defaults to `all`). +- You may pass multiple `--check` flags to combine specific checks. + +The `--stats` flag on `build_api_docs` is deprecated and routes to `check_package_docs`. diff --git a/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts b/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts index cc31c96dbb7f1..7dd4f139ac9a3 100644 --- a/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts +++ b/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts @@ -7,71 +7,113 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { createFlagError } from '@kbn/dev-cli-errors'; - -// Test flag validation logic directly without executing the full CLI -describe('build_api_docs_cli flag validation', () => { - // Test the validation logic that would be in build_api_docs_cli - function isStringArray(arr: unknown | string[]): arr is string[] { - return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); - } - - it('validates plugin flag must be string array', () => { - const pluginFilter = { invalid: 'object' }; - - if (pluginFilter && !isStringArray(pluginFilter)) { - expect(() => { - throw createFlagError('expected --plugin must only contain strings'); - }).toThrow('expected --plugin must only contain strings'); - } - }); +import apm from 'elastic-apm-node'; +import { runBuildApiDocsCli } from './build_api_docs_cli'; +import { + parseCliFlags, + setupProject, + buildApiMap, + collectStats, + reportMetrics, + writeDocs, +} from './cli'; +import { runCheckPackageDocs } from './check_package_docs_cli'; - it('validates stats flag values', () => { - const stats = ['invalid']; - - if ( - (stats && - isStringArray(stats) && - stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) || - (stats && !isStringArray(stats)) - ) { - expect(() => { - throw createFlagError( - 'expected --stats must only contain `any`, `comments` and/or `exports`' - ); - }).toThrow('expected --stats must only contain'); - } - }); +jest.mock('elastic-apm-node', () => { + const tx = { + startSpan: jest.fn(), + end: jest.fn(), + setOutcome: jest.fn(), + }; + return { + startTransaction: jest.fn(() => tx), + isStarted: jest.fn(() => false), + flush: jest.fn(), + __tx: tx, + }; +}); + +jest.mock('@kbn/apm-config-loader', () => ({ + initApm: jest.fn(), +})); + +let registeredHandler: any; +jest.mock('@kbn/dev-cli-runner', () => ({ + run: jest.fn((handler: any) => { + registeredHandler = handler; + }), +})); - it('accepts valid stats values', () => { - const stats = ['any', 'comments']; +jest.mock('./cli', () => ({ + parseCliFlags: jest.fn(), + setupProject: jest.fn(), + buildApiMap: jest.fn(), + collectStats: jest.fn(), + reportMetrics: jest.fn(), + writeDocs: jest.fn(), +})); - if ( - (stats && - isStringArray(stats) && - stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) || - (stats && !isStringArray(stats)) - ) { - throw new Error('Should not throw for valid stats'); - } +jest.mock('./check_package_docs_cli', () => ({ + runCheckPackageDocs: jest.fn(), +})); - // Should not throw - expect(stats).toEqual(['any', 'comments']); +const mockTx = (apm as any).__tx; + +describe('build_api_docs_cli', () => { + const log = { info: jest.fn(), warning: jest.fn(), error: jest.fn() }; + + beforeEach(() => { + registeredHandler = undefined; + jest.clearAllMocks(); }); - it('handles single string plugin flag', () => { - const plugin = 'single-plugin'; - const pluginFilter = typeof plugin === 'string' ? [plugin] : plugin; + it('routes --stats to check CLI and skips build tasks', async () => { + (parseCliFlags as jest.Mock).mockReturnValue({ stats: ['any'], collectReferences: false }); + + runBuildApiDocsCli(); + expect(registeredHandler).toBeDefined(); - expect(Array.isArray(pluginFilter)).toBe(true); - expect(pluginFilter).toEqual(['single-plugin']); + await registeredHandler({ log, flags: { stats: 'any' } }); + + expect(log.warning).toHaveBeenCalledWith(expect.stringContaining('--stats is deprecated')); + expect(runCheckPackageDocs).toHaveBeenCalledWith(log, { stats: 'any' }); + expect(setupProject).not.toHaveBeenCalled(); + expect(mockTx.end).toHaveBeenCalled(); }); - it('handles array plugin flag', () => { - const plugin = ['plugin1', 'plugin2']; - const pluginFilter = typeof plugin === 'string' ? [plugin] : plugin; + it('runs build flow when stats are not provided', async () => { + const setupResult = { + project: {}, + plugins: [ + { id: 'p1', manifest: { owner: { name: 'team' }, serviceFolders: [] }, isPlugin: true }, + ], + }; + const apiMapResult = { + pluginApiMap: {}, + missingApiItems: {}, + referencedDeprecations: {}, + unreferencedDeprecations: {}, + adoptionTrackedAPIs: {}, + }; + (parseCliFlags as jest.Mock).mockReturnValue({ stats: undefined, collectReferences: false }); + (setupProject as jest.Mock).mockResolvedValue(setupResult); + (buildApiMap as jest.Mock).mockReturnValue(apiMapResult); + (collectStats as jest.Mock).mockResolvedValue({}); + + runBuildApiDocsCli(); + await registeredHandler({ log, flags: {} }); - expect(Array.isArray(pluginFilter)).toBe(true); - expect(pluginFilter).toEqual(['plugin1', 'plugin2']); + expect(setupProject).toHaveBeenCalled(); + expect(buildApiMap).toHaveBeenCalledWith( + setupResult.project, + setupResult.plugins, + log, + mockTx, + { stats: undefined, collectReferences: false } + ); + expect(collectStats).toHaveBeenCalled(); + expect(reportMetrics).toHaveBeenCalled(); + expect(writeDocs).toHaveBeenCalled(); + expect(mockTx.end).toHaveBeenCalled(); }); }); diff --git a/packages/kbn-docs-utils/src/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/build_api_docs_cli.ts index 326f72285dfb8..5f3a42709ba45 100644 --- a/packages/kbn-docs-utils/src/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/build_api_docs_cli.ts @@ -26,9 +26,15 @@ import { type CliFlags, type CliContext, } from './cli'; +import { runCheckPackageDocs } from './check_package_docs_cli'; const rootDir = Path.join(__dirname, '../../..'); -initApm(process.argv, rootDir, false, 'build_api_docs_cli'); + +const startApm = () => { + if (!apm.isStarted()) { + initApm(process.argv, rootDir, false, 'build_api_docs_cli'); + } +}; async function endTransactionWithFailure(transaction: Transaction | null) { if (transaction !== null) { @@ -39,10 +45,10 @@ async function endTransactionWithFailure(transaction: Transaction | null) { } export function runBuildApiDocsCli() { + startApm(); run( async ({ log, flags }) => { const transaction = apm.startTransaction('build-api-docs', 'kibana-cli'); - const spanSetup = transaction.startSpan('build_api_docs.setup', 'setup'); let options; try { @@ -52,6 +58,16 @@ export function runBuildApiDocsCli() { throw error; } + if (options.stats && options.stats.length > 0) { + log.warning('--stats is deprecated. Please run check_package_docs_cli instead.'); + transaction?.end(); + await apm.flush(); + await runCheckPackageDocs(log, flags as CliFlags); + return; + } + + const spanSetup = transaction.startSpan('build_api_docs.setup', 'setup'); + const outputFolder = Path.resolve(REPO_ROOT, 'api_docs'); const context: CliContext = { @@ -101,14 +117,14 @@ export function runBuildApiDocsCli() { defaultLevel: 'info', }, flags: { - string: ['plugin', 'stats'], + string: ['plugin', 'package', 'stats'], boolean: ['references'], help: ` - --plugin Optionally, run for only a specific plugin - --stats Optionally print API stats. Must be one or more of: any, comments or exports. - In combination with a single plugin filter this option will skip writing any - API docs as a tradeoff to just produce the stats output more quickly. - --references Collect references for API items + --plugin Optionally, run for only a specific plugin by its plugin ID (plugin.id in kibana.jsonc). + --package Optionally, run for only a specific package by its package ID (id in kibana.jsonc, e.g., @kbn/core). + --stats Deprecated. Use check_package_docs_cli instead. When provided, validation is routed + to the new CLI and build outputs are skipped. Must be one or more of: any, comments or exports. + --references Collect references for API items. `, }, } diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.run.test.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.run.test.ts new file mode 100644 index 0000000000000..893bad4980a78 --- /dev/null +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.run.test.ts @@ -0,0 +1,135 @@ +/* + * 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 apm from 'elastic-apm-node'; +import { runCheckPackageDocs } from './check_package_docs_cli'; +import { parseCliFlags, setupProject, buildApiMap, collectStats, reportMetrics } from './cli'; + +jest.mock('elastic-apm-node', () => { + const tx = { + startSpan: jest.fn(), + end: jest.fn(), + setOutcome: jest.fn(), + }; + return { + startTransaction: jest.fn(() => tx), + isStarted: jest.fn(() => false), + flush: jest.fn(), + __tx: tx, + }; +}); + +jest.mock('@kbn/apm-config-loader', () => ({ + initApm: jest.fn(), +})); + +jest.mock('./cli', () => ({ + parseCliFlags: jest.fn(), + setupProject: jest.fn(), + buildApiMap: jest.fn(), + collectStats: jest.fn(), + reportMetrics: jest.fn(), +})); + +const mockTx = (apm as any).__tx; + +const plugin = { + id: 'plugin-a', + manifest: { owner: { name: 'team' }, serviceFolders: [] }, + isPlugin: true, +}; + +describe('runCheckPackageDocs', () => { + const log = { info: jest.fn(), warning: jest.fn(), error: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + process.exitCode = undefined; + }); + + it('sets exitCode when validation fails', async () => { + (parseCliFlags as jest.Mock).mockReturnValue({ stats: ['any'], pluginFilter: ['plugin-a'] }); + (setupProject as jest.Mock).mockResolvedValue({ plugins: [plugin], project: {} }); + (buildApiMap as jest.Mock).mockReturnValue({ + pluginApiMap: { 'plugin-a': { id: 'plugin-a', client: [], server: [], common: [] } }, + missingApiItems: { 'plugin-a': { 'src/path.ts': ['ref'] } }, + referencedDeprecations: {}, + unreferencedDeprecations: {}, + adoptionTrackedAPIs: {}, + }); + (collectStats as jest.Mock).mockResolvedValue({ + 'plugin-a': { + missingComments: [], + isAnyType: [{ id: 'x' }], + noReferences: [], + apiCount: 1, + missingExports: 1, + deprecatedAPIsReferencedCount: 0, + unreferencedDeprecatedApisCount: 0, + adoptionTrackedAPIs: [], + adoptionTrackedAPIsCount: 0, + adoptionTrackedAPIsUnreferencedCount: 0, + owner: { name: 'team' }, + description: '', + isPlugin: true, + eslintDisableFileCount: 0, + eslintDisableLineCount: 0, + enzymeImportCount: 0, + }, + }); + + await runCheckPackageDocs(log as any, { plugin: 'plugin-a' } as any); + + expect(parseCliFlags).toHaveBeenCalledWith({ plugin: 'plugin-a' }); + expect(reportMetrics).toHaveBeenCalled(); + expect(process.exitCode).toBe(1); + expect(log.error).toHaveBeenCalledWith( + expect.stringContaining('Validation failed for 1 plugin') + ); + expect(mockTx.end).toHaveBeenCalled(); + }); + + it('passes when there are no validation issues', async () => { + (parseCliFlags as jest.Mock).mockReturnValue({ stats: ['any'], pluginFilter: ['plugin-a'] }); + (setupProject as jest.Mock).mockResolvedValue({ plugins: [plugin], project: {} }); + (buildApiMap as jest.Mock).mockReturnValue({ + pluginApiMap: { 'plugin-a': { id: 'plugin-a', client: [], server: [], common: [] } }, + missingApiItems: {}, + referencedDeprecations: {}, + unreferencedDeprecations: {}, + adoptionTrackedAPIs: {}, + }); + (collectStats as jest.Mock).mockResolvedValue({ + 'plugin-a': { + missingComments: [], + isAnyType: [], + noReferences: [], + apiCount: 1, + missingExports: 0, + deprecatedAPIsReferencedCount: 0, + unreferencedDeprecatedApisCount: 0, + adoptionTrackedAPIs: [], + adoptionTrackedAPIsCount: 0, + adoptionTrackedAPIsUnreferencedCount: 0, + owner: { name: 'team' }, + description: '', + isPlugin: true, + eslintDisableFileCount: 0, + eslintDisableLineCount: 0, + enzymeImportCount: 0, + }, + }); + + await runCheckPackageDocs(log as any, { plugin: 'plugin-a' } as any); + + expect(process.exitCode).toBeUndefined(); + expect(log.info).toHaveBeenCalledWith('All plugins passed validation.'); + expect(mockTx.end).toHaveBeenCalled(); + }); +}); diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts new file mode 100644 index 0000000000000..69293b4930a4b --- /dev/null +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { getValidationResults } from './check_package_docs_cli'; +import { + TypeKind, + type ApiDeclaration, + type PluginOrPackage, + type MissingApiItemMap, +} from './types'; +import type { AllPluginStats } from './cli/types'; + +const createApiDeclaration = (id: string, parentPluginId: string): ApiDeclaration => ({ + id, + label: id, + type: TypeKind.FunctionKind, + path: `path/${id}`, + parentPluginId, +}); + +const createPlugin = (id: string, isPlugin = true): PluginOrPackage => ({ + id, + manifest: { + id, + description: `${id} description`, + owner: { name: 'team' }, + serviceFolders: [], + }, + isPlugin, + directory: `/tmp/${id}`, + manifestPath: `/tmp/${id}/kibana.json`, +}); + +const createBaseStats = (pluginId: string): AllPluginStats => ({ + [pluginId]: { + missingComments: [], + isAnyType: [], + noReferences: [], + apiCount: 0, + missingExports: 0, + deprecatedAPIsReferencedCount: 0, + unreferencedDeprecatedApisCount: 0, + adoptionTrackedAPIs: [], + adoptionTrackedAPIsCount: 0, + adoptionTrackedAPIsUnreferencedCount: 0, + owner: { name: 'team' }, + description: `${pluginId} description`, + isPlugin: true, + eslintDisableFileCount: 0, + eslintDisableLineCount: 0, + enzymeImportCount: 0, + }, +}); + +describe('getValidationResults', () => { + it('passes plugins without issues', () => { + const pluginId = 'plugin-a'; + const plugins = [createPlugin(pluginId)]; + const missingApiItems: MissingApiItemMap = {}; + const allPluginStats = createBaseStats(pluginId); + + const results = getValidationResults( + plugins, + missingApiItems, + ['any', 'comments', 'exports'], + undefined, + undefined, + allPluginStats + ); + + expect(results).toEqual([{ pluginId, passed: true }]); + }); + + it('fails plugins with selected validation issues', () => { + const pluginId = 'plugin-b'; + const plugins = [createPlugin(pluginId)]; + const missingApiItems: MissingApiItemMap = { + [pluginId]: { 'src/path.ts': ['ref'] }, + }; + const allPluginStats = { + ...createBaseStats(pluginId), + [pluginId]: { + ...createBaseStats(pluginId)[pluginId], + isAnyType: [createApiDeclaration('anyIssue', pluginId)], + missingComments: [createApiDeclaration('commentIssue', pluginId)], + }, + }; + + const results = getValidationResults( + plugins, + missingApiItems, + ['any', 'exports'], + undefined, + undefined, + allPluginStats + ); + + expect(results).toEqual([{ pluginId, passed: false }]); + }); + + it('applies plugin filters', () => { + const plugins = [createPlugin('plugin-a'), createPlugin('plugin-b')]; + const missingApiItems: MissingApiItemMap = { + 'plugin-b': { 'src/path.ts': ['ref'] }, + }; + const allPluginStats: AllPluginStats = { + ...createBaseStats('plugin-a'), + ...createBaseStats('plugin-b'), + }; + + const results = getValidationResults( + plugins, + missingApiItems, + ['exports'], + ['plugin-a'], + undefined, + allPluginStats + ); + + expect(results).toEqual([{ pluginId: 'plugin-a', passed: true }]); + }); +}); diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.ts new file mode 100644 index 0000000000000..880ba3ad7e583 --- /dev/null +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.ts @@ -0,0 +1,187 @@ +/* + * 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 Path from 'path'; + +import apm from 'elastic-apm-node'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { run } from '@kbn/dev-cli-runner'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { initApm } from '@kbn/apm-config-loader'; + +import { + parseCliFlags, + setupProject, + buildApiMap, + collectStats, + reportMetrics, + type CliFlags, + type CliContext, + type CliOptions, +} from './cli'; +import type { PluginOrPackage, MissingApiItemMap } from './types'; +import type { AllPluginStats } from './cli/types'; + +type ValidationCheck = 'any' | 'comments' | 'exports'; + +const DEFAULT_VALIDATION_CHECKS: ValidationCheck[] = ['any', 'comments', 'exports']; + +const rootDir = Path.join(__dirname, '../../..'); + +const startApm = () => { + if (!apm.isStarted()) { + initApm(process.argv, rootDir, false, 'check_package_docs_cli'); + } +}; + +interface ValidationResult { + pluginId: string; + passed: boolean; +} + +const resolveValidationChecks = (options: CliOptions): ValidationCheck[] => { + const checks = + options.stats && options.stats.length > 0 ? options.stats : DEFAULT_VALIDATION_CHECKS; + return checks as ValidationCheck[]; +}; + +export const getValidationResults = ( + plugins: PluginOrPackage[], + missingApiItems: MissingApiItemMap, + checks: ValidationCheck[], + pluginFilter: string[] | undefined, + packageFilter: string[] | undefined, + allPluginStats: AllPluginStats +): ValidationResult[] => { + const shouldCheckAny = checks.includes('any'); + const shouldCheckComments = checks.includes('comments'); + const shouldCheckExports = checks.includes('exports'); + + const hasPluginFilter = pluginFilter && pluginFilter.length > 0; + const hasPackageFilter = packageFilter && packageFilter.length > 0; + + return plugins + .filter((plugin) => { + if (!hasPluginFilter && !hasPackageFilter) return true; + if (plugin.isPlugin && hasPluginFilter) return pluginFilter.includes(plugin.id); + if (!plugin.isPlugin && hasPackageFilter) return packageFilter.includes(plugin.id); + return false; + }) + .map((plugin) => { + const pluginStats = allPluginStats[plugin.id]; + if (!pluginStats) { + return { pluginId: plugin.id, passed: true }; + } + const missingExports = missingApiItems[plugin.id] + ? Object.keys(missingApiItems[plugin.id]).length + : 0; + + const hasAnyIssues = shouldCheckAny && pluginStats.isAnyType.length > 0; + const hasCommentIssues = shouldCheckComments && pluginStats.missingComments.length > 0; + const hasExportIssues = shouldCheckExports && missingExports > 0; + + return { + pluginId: plugin.id, + passed: !(hasAnyIssues || hasCommentIssues || hasExportIssues), + }; + }); +}; + +export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => { + startApm(); + const transaction = apm.startTransaction('check-package-docs', 'kibana-cli'); + + try { + const options = parseCliFlags(flags); + const checks = resolveValidationChecks(options); + const optionsWithChecks: CliOptions = { + ...options, + stats: checks, + }; + + const outputFolder = Path.resolve(REPO_ROOT, 'api_docs_check'); + const context: CliContext = { + log, + transaction, + outputFolder, + }; + + const setupResult = await setupProject(context, optionsWithChecks); + const apiMapResult = buildApiMap( + setupResult.project, + setupResult.plugins, + log, + transaction, + optionsWithChecks + ); + + const allPluginStats = await collectStats( + setupResult, + apiMapResult, + log, + transaction, + optionsWithChecks + ); + + reportMetrics(setupResult, apiMapResult, allPluginStats, log, transaction, { + ...optionsWithChecks, + stats: checks, + }); + + const validationResults = getValidationResults( + setupResult.plugins, + apiMapResult.missingApiItems, + checks, + optionsWithChecks.pluginFilter, + optionsWithChecks.packageFilter, + allPluginStats + ); + + const failingPlugins = validationResults.filter((result) => !result.passed); + + if (failingPlugins.length > 0) { + log.error( + `Validation failed for ${failingPlugins.length} plugin(s): ${failingPlugins + .map((plugin) => plugin.pluginId) + .join(', ')}.` + ); + process.exitCode = 1; + } else { + log.info('All plugins passed validation.'); + } + } catch (error) { + transaction?.setOutcome('failure'); + throw error; + } finally { + transaction?.end(); + await apm.flush(); + } +}; + +export const runCheckPackageDocsCli = () => { + run( + async ({ log, flags }) => { + await runCheckPackageDocs(log, flags as CliFlags); + }, + { + log: { + defaultLevel: 'info', + }, + flags: { + string: ['plugin', 'package', 'check'], + help: ` + --plugin Optionally, run for only a specific plugin by its plugin ID (plugin.id in kibana.jsonc). + --package Optionally, run for only a specific package by its package ID (id in kibana.jsonc, e.g., @kbn/core). + --check Optional. Specify validation checks: any, comments, exports, or all (default). + Can be provided multiple times to combine checks. + `, + }, + } + ); +}; diff --git a/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts b/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts index 1c4c79e51190b..71336e9ca1e8a 100644 --- a/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts +++ b/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts @@ -35,6 +35,18 @@ describe('parseCliFlags', () => { expect(result.pluginFilter).toEqual(['single-plugin']); }); + it('keeps plugin and package filters separate', () => { + const flags: CliFlags = { + plugin: 'plugin-a', + package: ['package-b', 'package-a'], + }; + + const result = parseCliFlags(flags); + + expect(result.pluginFilter).toEqual(['plugin-a']); + expect(result.packageFilter).toEqual(['package-b', 'package-a']); + }); + it('normalizes single string stats to array', () => { const flags: CliFlags = { stats: 'any', @@ -53,6 +65,38 @@ describe('parseCliFlags', () => { expect(result.collectReferences).toBe(false); expect(result.stats).toBeUndefined(); expect(result.pluginFilter).toBeUndefined(); + expect(result.packageFilter).toBeUndefined(); + }); + + it('accepts single check flag', () => { + const flags: CliFlags = { + check: 'any', + }; + + const result = parseCliFlags(flags); + + expect(result.stats).toEqual(['any']); + }); + + it('accepts multiple check flags and expands all', () => { + const flags: CliFlags = { + check: ['comments', 'all'], + }; + + const result = parseCliFlags(flags); + + expect(result.stats).toEqual(['comments', 'any', 'exports']); + }); + + it('dedupes overlapping stats and check flags', () => { + const flags: CliFlags = { + stats: ['any', 'comments'], + check: ['comments'], + }; + + const result = parseCliFlags(flags); + + expect(result.stats).toEqual(['any', 'comments']); }); it('throws error for invalid plugin filter type', () => { diff --git a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts index 0bd25c64a8bfe..771113736fd42 100644 --- a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts +++ b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts @@ -13,9 +13,84 @@ import type { CliFlags, CliOptions } from './types'; /** * Validates that an array contains only strings. */ -function isStringArray(arr: unknown | string[]): arr is string[] { - return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); -} +const isStringArray = (arr: unknown | string[]): arr is string[] => + Array.isArray(arr) && arr.every((p) => typeof p === 'string'); + +const normalizeStringList = (value: unknown | string[], flagName: string) => { + if (!value) { + return undefined; + } + + const normalized = typeof value === 'string' ? [value] : value; + + if (!isStringArray(normalized)) { + throw createFlagError(`expected --${flagName} must only contain strings`); + } + + return normalized; +}; + +const dedupe = (values: string[] | undefined) => + values && values.length > 0 ? Array.from(new Set(values)) : undefined; + +const VALID_CHECKS = ['any', 'comments', 'exports', 'all'] as const; + +const normalizeCheckFlagValues = (check: unknown | string[]) => { + if (!check) { + return undefined; + } + + if (typeof check === 'string') { + return [check]; + } + + if (!isStringArray(check)) { + throw createFlagError( + 'expected --check must only contain `any`, `comments`, `exports`, or `all`' + ); + } + + return check; +}; + +const expandChecks = (checks: string[] | undefined) => { + if (!checks) { + return undefined; + } + + const expanded = checks.flatMap((c) => (c === 'all' ? ['any', 'comments', 'exports'] : [c])); + + const invalid = expanded.find((c) => !VALID_CHECKS.includes(c as (typeof VALID_CHECKS)[number])); + if (invalid) { + throw createFlagError( + 'expected --check must only contain `any`, `comments`, `exports`, or `all`' + ); + } + + return expanded; +}; + +const validateStats = (stats: string[] | undefined) => { + if (stats && stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) { + throw createFlagError('expected --stats must only contain `any`, `comments` and/or `exports`'); + } +}; + +const normalizeStats = (value: unknown | string[]) => { + if (!value) { + return undefined; + } + + if (typeof value === 'string') { + return [value]; + } + + if (!isStringArray(value)) { + throw createFlagError('expected --stats must only contain `any`, `comments` and/or `exports`'); + } + + return value; +}; /** * Parses and validates CLI flags, normalizing them into a consistent format. @@ -26,28 +101,19 @@ function isStringArray(arr: unknown | string[]): arr is string[] { */ export function parseCliFlags(flags: CliFlags): CliOptions { const collectReferences = flags.references === true; - const stats = flags.stats && typeof flags.stats === 'string' ? [flags.stats] : flags.stats; - const pluginFilter = - flags.plugin && typeof flags.plugin === 'string' - ? [flags.plugin] - : (flags.plugin as string[] | undefined); - - if (pluginFilter && !isStringArray(pluginFilter)) { - throw createFlagError('expected --plugin must only contain strings'); - } - - if ( - (stats && - isStringArray(stats) && - stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) || - (stats && !isStringArray(stats)) - ) { - throw createFlagError('expected --stats must only contain `any`, `comments` and/or `exports`'); - } + const pluginFilter = dedupe(normalizeStringList(flags.plugin, 'plugin')); + const packageFilter = dedupe(normalizeStringList(flags.package, 'package')); + const rawStats = normalizeStats(flags.stats); + const rawChecks = normalizeCheckFlagValues(flags.check); + const expandedChecks = expandChecks(rawChecks); + const stats = dedupe([...(rawStats ?? []), ...(expandedChecks ?? [])]); + + validateStats(stats); return { collectReferences, - stats: stats && isStringArray(stats) ? stats : undefined, + stats, pluginFilter, + packageFilter, }; } diff --git a/packages/kbn-docs-utils/src/cli/run_check_package_docs_cli.ts b/packages/kbn-docs-utils/src/cli/run_check_package_docs_cli.ts new file mode 100644 index 0000000000000..24287f132355e --- /dev/null +++ b/packages/kbn-docs-utils/src/cli/run_check_package_docs_cli.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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". + */ + +export { runCheckPackageDocsCli } from '../check_package_docs_cli'; diff --git a/packages/kbn-docs-utils/src/cli/tasks/setup_project.test.ts b/packages/kbn-docs-utils/src/cli/tasks/setup_project.test.ts index a65a3a2bec0c3..4009496c575b9 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/setup_project.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/setup_project.test.ts @@ -134,6 +134,10 @@ describe('setupProject', () => { const Fs = jest.requireMock('fs'); Fs.existsSync.mockReturnValue(true); + (findPlugins as jest.Mock).mockReturnValue([ + { id: 'test-plugin', isPlugin: true, directory: '/tmp/test' }, + ]); + const options: CliOptions = { collectReferences: false, pluginFilter: ['test-plugin'], @@ -149,10 +153,24 @@ describe('setupProject', () => { const options: CliOptions = { collectReferences: false, - stats: ['any'], pluginFilter: ['nonexistent-plugin'], }; - await expect(setupProject(context, options)).rejects.toThrow('expected --plugin was not found'); + await expect(setupProject(context, options)).rejects.toThrow( + "expected --plugin 'nonexistent-plugin' was not found" + ); + }); + + it('validates package filter and throws error if packages not found', async () => { + (findPlugins as jest.Mock).mockReturnValue([]); + + const options: CliOptions = { + collectReferences: false, + packageFilter: ['@kbn/nonexistent-package'], + }; + + await expect(setupProject(context, options)).rejects.toThrow( + "expected --package '@kbn/nonexistent-package' was not found" + ); }); }); diff --git a/packages/kbn-docs-utils/src/cli/tasks/setup_project.ts b/packages/kbn-docs-utils/src/cli/tasks/setup_project.ts index 2cbab7ec341ce..f8c828945a037 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/setup_project.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/setup_project.ts @@ -73,24 +73,35 @@ export async function setupProject( ): Promise { const { transaction, outputFolder } = context; + const { pluginFilter, packageFilter } = options; + const hasPluginFilter = pluginFilter && pluginFilter.length > 0; + const hasPackageFilter = packageFilter && packageFilter.length > 0; + const hasAnyFilter = hasPluginFilter || hasPackageFilter; + const spanInitialDocIds = transaction.startSpan('build_api_docs.initialDocIds', 'setup'); const initialDocIds = - !options.pluginFilter && Fs.existsSync(outputFolder) - ? await getAllDocFileIds(outputFolder) - : undefined; + !hasAnyFilter && Fs.existsSync(outputFolder) ? await getAllDocFileIds(outputFolder) : undefined; spanInitialDocIds?.end(); const spanPlugins = transaction.startSpan('build_api_docs.findPlugins', 'setup'); - const plugins = findPlugins( - options.stats && options.pluginFilter ? options.pluginFilter : undefined - ); + const plugins = hasAnyFilter ? findPlugins({ pluginFilter, packageFilter }) : findPlugins(); + + // Validate that all requested plugins were found. + if (hasPluginFilter && pluginFilter) { + const foundPluginIds = plugins.filter((p) => p.isPlugin).map((p) => p.id); + const missingPlugins = pluginFilter.filter((id) => !foundPluginIds.includes(id)); + if (missingPlugins.length > 0) { + throw createFlagError(`expected --plugin '${missingPlugins.join(', ')}' was not found`); + } + } - if ( - options.stats && - Array.isArray(options.pluginFilter) && - options.pluginFilter.length !== plugins.length - ) { - throw createFlagError('expected --plugin was not found'); + // Validate that all requested packages were found. + if (hasPackageFilter && packageFilter) { + const foundPackageIds = plugins.filter((p) => !p.isPlugin).map((p) => p.id); + const missingPackages = packageFilter.filter((id) => !foundPackageIds.includes(id)); + if (missingPackages.length > 0) { + throw createFlagError(`expected --package '${missingPackages.join(', ')}' was not found`); + } } spanPlugins?.end(); @@ -99,15 +110,15 @@ export async function setupProject( spanPathsByPackage?.end(); const spanProject = transaction.startSpan('build_api_docs.getTsProject', 'setup'); - const project = getTsProject( - REPO_ROOT, - options.stats && options.pluginFilter && plugins.length === 1 ? plugins[0].directory : undefined - ); + // Optimize: when building a single plugin/package, scope the TS project to just that directory + const singlePluginDirectory = + hasAnyFilter && plugins.length === 1 ? plugins[0].directory : undefined; + const project = getTsProject(REPO_ROOT, singlePluginDirectory); spanProject?.end(); const spanFolders = transaction.startSpan('build_api_docs.check-folders', 'setup'); - // if the output folder already exists, and we don't have a plugin filter, delete all the files in the output folder - if (Fs.existsSync(outputFolder) && !options.pluginFilter) { + // if the output folder already exists, and we don't have a filter, delete all the files in the output folder + if (Fs.existsSync(outputFolder) && !hasAnyFilter) { await Fsp.rm(outputFolder, { recursive: true }); } diff --git a/packages/kbn-docs-utils/src/cli/types.ts b/packages/kbn-docs-utils/src/cli/types.ts index 7146e1b736a9b..2e0f01c32f6fa 100644 --- a/packages/kbn-docs-utils/src/cli/types.ts +++ b/packages/kbn-docs-utils/src/cli/types.ts @@ -31,8 +31,12 @@ export interface CliFlags { references?: boolean; /** Stats flags: 'any', 'comments', and/or 'exports'. */ stats?: string | string[]; - /** Plugin filter: single plugin ID or array of plugin IDs. */ + /** Validation checks: 'any', 'comments', 'exports', or 'all'. */ + check?: string | string[]; + /** Plugin filter: single plugin ID or array of plugin IDs (plugin.id from kibana.jsonc). */ plugin?: string | string[]; + /** Package filter: single package ID or array of package IDs (id from kibana.jsonc). */ + package?: string | string[]; } /** @@ -43,8 +47,10 @@ export interface CliOptions { collectReferences: boolean; /** Stats flags to display. */ stats?: string[]; - /** Plugin filter IDs. */ + /** Plugin filter IDs (plugin.id from kibana.jsonc). */ pluginFilter?: string[]; + /** Package filter IDs (id from kibana.jsonc, e.g., @kbn/package-name). */ + packageFilter?: string[]; } /** diff --git a/packages/kbn-docs-utils/src/find_plugins.test.ts b/packages/kbn-docs-utils/src/find_plugins.test.ts index fa552324cb337..a78396aa23182 100644 --- a/packages/kbn-docs-utils/src/find_plugins.test.ts +++ b/packages/kbn-docs-utils/src/find_plugins.test.ts @@ -22,8 +22,8 @@ describe('findPlugins', () => { expect(core!.isPlugin).toBe(false); }); - it('filters plugins when pluginOrPackageFilter is provided', () => { - const result = findPlugins(['data']); + it('filters plugins when pluginFilter is provided', () => { + const result = findPlugins({ pluginFilter: ['data'] }); expect(Array.isArray(result)).toBe(true); // All returned plugins should have 'data' in their ID. result.forEach((plugin) => { @@ -31,6 +31,13 @@ describe('findPlugins', () => { }); }); + it('filters packages when packageFilter is provided', () => { + const result = findPlugins({ packageFilter: ['@kbn/core'] }); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + expect(result.every((p) => !p.isPlugin)).toBe(true); + }); + it('includes packages in the result', () => { const result = findPlugins(); const packages = result.filter((p) => !p.isPlugin); diff --git a/packages/kbn-docs-utils/src/find_plugins.ts b/packages/kbn-docs-utils/src/find_plugins.ts index a5987240b2e78..b206e7800370c 100644 --- a/packages/kbn-docs-utils/src/find_plugins.ts +++ b/packages/kbn-docs-utils/src/find_plugins.ts @@ -69,7 +69,24 @@ function toPluginOrPackage(pkg: Package): PluginOrPackage { return result; } -export function findPlugins(pluginOrPackageFilter?: string[]): PluginOrPackage[] { +/** + * Options for filtering plugins and packages. + */ +export interface FindPluginsOptions { + /** Plugin IDs to filter by (plugin.id from kibana.jsonc). */ + pluginFilter?: string[]; + /** Package IDs to filter by (id from kibana.jsonc, e.g., @kbn/package-name). */ + packageFilter?: string[]; +} + +/** + * Finds all plugins and packages that should have API documentation generated. + * + * @param options - Optional filter options for plugins and packages. + * @returns Array of plugins and packages to document. + */ +export function findPlugins(options?: FindPluginsOptions): PluginOrPackage[] { + const { pluginFilter, packageFilter } = options ?? {}; const packages = getPackages(REPO_ROOT); const plugins = packages.filter( getPluginPackagesFilter({ @@ -83,16 +100,28 @@ export function findPlugins(pluginOrPackageFilter?: string[]): PluginOrPackage[] throw new Error('unable to find @kbn/core'); } - if (!pluginOrPackageFilter) { + const hasPluginFilter = pluginFilter && pluginFilter.length > 0; + const hasPackageFilter = packageFilter && packageFilter.length > 0; + + if (!hasPluginFilter && !hasPackageFilter) { return [...[core, ...plugins].map(toPluginOrPackage), ...findPackages()]; - } else { - return [ - ...plugins - .filter((p) => pluginOrPackageFilter.includes(p.manifest.plugin.id)) - .map(toPluginOrPackage), - ...findPackages(pluginOrPackageFilter), - ]; } + + const result: PluginOrPackage[] = []; + + // Filter plugins by plugin.id. + if (hasPluginFilter) { + result.push( + ...plugins.filter((p) => pluginFilter.includes(p.manifest.plugin.id)).map(toPluginOrPackage) + ); + } + + // Filter packages by manifest.id. + if (hasPackageFilter) { + result.push(...findPackages(packageFilter)); + } + + return result; } export function findTeamPlugins(team: string): PluginOrPackage[] { diff --git a/packages/kbn-docs-utils/src/index.ts b/packages/kbn-docs-utils/src/index.ts index e9a0310fa72f5..0fd49f0cd359e 100644 --- a/packages/kbn-docs-utils/src/index.ts +++ b/packages/kbn-docs-utils/src/index.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './build_api_docs_cli'; +export { runBuildApiDocsCli } from './build_api_docs_cli'; +export { runCheckPackageDocsCli } from './check_package_docs_cli'; diff --git a/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts b/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts index 01f6224d4099c..a270695b0ad39 100644 --- a/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts +++ b/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts @@ -719,6 +719,21 @@ describe('validation and stats', () => { } }); + it.skip('does not flag destructured params when `@param obj` exists', () => { + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + expect(objParam!.description).toBeDefined(); + // Expected once fixed: the @param obj comment is captured. + expect(objParam!.description!.length).toBeGreaterThan(0); + + const missingComment = pluginAStats.missingComments.find((d) => d.id === objParam!.id); + // Expected once fixed: the destructured param should not be flagged as missing. + expect(missingComment).toBeUndefined(); + }); + it('validates missingComments does not include APIs with descriptions', () => { // notAnArrowFn has complete JSDoc const fn = doc.client.find((c) => c.label === 'notAnArrowFn'); diff --git a/packages/kbn-docs-utils/src/utils.ts b/packages/kbn-docs-utils/src/utils.ts index e054039852020..c80c6371c0c6d 100644 --- a/packages/kbn-docs-utils/src/utils.ts +++ b/packages/kbn-docs-utils/src/utils.ts @@ -192,9 +192,11 @@ export function removeBrokenLinks( }); }); - if (missingCnt > 0) { + const uniqueMissing = Object.keys(missingApiItems[pluginApi.id] ?? {}).length; + + if (uniqueMissing > 0) { log.info( - `${pluginApi.id} had ${missingCnt} API item references removed to avoid broken links use the flag '--stats exports' to get a list of every missing export ` + `${pluginApi.id} had ${uniqueMissing} missing exported API item(s). Removed ${missingCnt} reference(s) to avoid broken links. Use '--stats exports' to list missing exports.` ); } } diff --git a/packages/kbn-plugin-check/lib/get_plugin.test.ts b/packages/kbn-plugin-check/lib/get_plugin.test.ts new file mode 100644 index 0000000000000..0dc27dfa589c2 --- /dev/null +++ b/packages/kbn-plugin-check/lib/get_plugin.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { ToolingLog } from '@kbn/tooling-log'; + +import { getPlugin } from './get_plugin'; + +jest.mock('@kbn/docs-utils', () => ({ + findPlugins: jest.fn(), +})); + +const { findPlugins } = jest.requireMock('@kbn/docs-utils'); + +interface MockPlugin { + id: string; + manifest: { id: string; owner: { name: string }; serviceFolders: readonly string[] }; + isPlugin: boolean; + directory: string; + manifestPath: string; +} + +const createMockPlugin = (id: string): MockPlugin => ({ + id, + manifest: { + id: `@kbn/${id}`, + owner: { name: 'Test Owner' }, + serviceFolders: [], + }, + isPlugin: true, + directory: `/path/to/${id}`, + manifestPath: `/path/to/${id}/kibana.jsonc`, +}); + +const createMockLog = (): jest.Mocked => + ({ + debug: jest.fn(), + error: jest.fn(), + } as unknown as jest.Mocked); + +describe('getPlugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the plugin when found', () => { + const mockPlugin = createMockPlugin('testPlugin'); + findPlugins.mockReturnValue([mockPlugin]); + const log = createMockLog(); + + const result = getPlugin('testPlugin', log); + + expect(findPlugins).toHaveBeenCalledWith({ pluginFilter: ['testPlugin'] }); + expect(result).toEqual(mockPlugin); + expect(log.debug).toHaveBeenCalledWith('Found plugin:', 'testPlugin'); + expect(log.error).not.toHaveBeenCalled(); + }); + + it('returns null and logs error when plugin is not found', () => { + findPlugins.mockReturnValue([]); + const log = createMockLog(); + + const result = getPlugin('nonExistentPlugin', log); + + expect(findPlugins).toHaveBeenCalledWith({ pluginFilter: ['nonExistentPlugin'] }); + expect(result).toBeNull(); + expect(log.error).toHaveBeenCalledWith('Plugin nonExistentPlugin not found'); + expect(log.debug).not.toHaveBeenCalled(); + }); + + it('returns null when `findPlugins` returns plugins that do not match the requested id', () => { + const mockPlugin = createMockPlugin('differentPlugin'); + findPlugins.mockReturnValue([mockPlugin]); + const log = createMockLog(); + + const result = getPlugin('testPlugin', log); + + expect(result).toBeNull(); + expect(log.error).toHaveBeenCalledWith('Plugin testPlugin not found'); + }); + + it('finds the correct plugin when multiple plugins are returned', () => { + const plugins = [ + createMockPlugin('pluginA'), + createMockPlugin('targetPlugin'), + createMockPlugin('pluginB'), + ]; + findPlugins.mockReturnValue(plugins); + const log = createMockLog(); + + const result = getPlugin('targetPlugin', log); + + expect(result).toEqual(plugins[1]); + expect(log.debug).toHaveBeenCalledWith('Found plugin:', 'targetPlugin'); + }); +}); diff --git a/packages/kbn-plugin-check/lib/get_plugin.ts b/packages/kbn-plugin-check/lib/get_plugin.ts index 3a8beb90ea24e..21b0733470776 100644 --- a/packages/kbn-plugin-check/lib/get_plugin.ts +++ b/packages/kbn-plugin-check/lib/get_plugin.ts @@ -14,7 +14,11 @@ import type { ToolingLog } from '@kbn/tooling-log'; * Utility method for finding and logging information about a plugin. */ export const getPlugin = (pluginName: string, log: ToolingLog) => { - const plugin = findPlugins([pluginName])[0]; + const plugin = findPlugins({ pluginFilter: [pluginName] }).find((p) => p.id === pluginName); + if (!plugin) { + log.error(`Plugin ${pluginName} not found`); + return null; + } log.debug('Found plugin:', pluginName); return plugin; }; diff --git a/scripts/check_package_docs.js b/scripts/check_package_docs.js new file mode 100644 index 0000000000000..cd322548321cf --- /dev/null +++ b/scripts/check_package_docs.js @@ -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('@kbn/setup-node-env'); +require('@kbn/docs-utils').runCheckPackageDocsCli();