diff --git a/packages/kbn-docs-utils/src/README.md b/packages/kbn-docs-utils/src/README.md index 41906ef8fa16a..ae93d98bd4141 100644 --- a/packages/kbn-docs-utils/src/README.md +++ b/packages/kbn-docs-utils/src/README.md @@ -1,25 +1,26 @@ -# Autogenerated API documentation +# Autogenerated API documentation. -[RFC](https://github.com/elastic/kibana/blob/main/legacy_rfcs/text/0014_api_documentation.md)) +[RFC](https://github.com/elastic/kibana/blob/main/legacy_rfcs/text/0014_api_documentation.md). -This is an experimental api documentation system that is managed by the Kibana Tech Leads until -we determine the value of such a system and what kind of maintenance burden it will incur. +This package builds and validates API documentation for Kibana plugins and packages. Use `node scripts/build_api_docs` to emit docs to `api_docs/`, or `node scripts/check_package_docs` to validate JSDoc without writing files. -To generate the docs run +## CLI commands. -``` -node scripts/build_api_docs -``` +### Build API docs (`node scripts/build_api_docs`). +- Generates docs into `api_docs/` using [`src/build_api_docs_cli.ts`](./build_api_docs_cli.ts). +- `--plugin ` limits to a single plugin or package; `--package` is an alias. +- `--references` collects references for API items. +- `--stats ` is deprecated and routes validation to `check_package_docs` without writing docs. -To validate documentation without writing files, run +### Check package docs (`node scripts/check_package_docs`). +- Runs validation only (no docs written) via [`src/check_package_docs_cli.ts`](./check_package_docs_cli.ts); output folder is `api_docs_check/`. +- `--plugin ` and `--package ` filter targets; omit to check all plugins. +- `--check ` selects checks; defaults to `all` (equivalent to `any`, `comments`, and `exports`). +- Multiple `--check` flags combine checks. +- Exits with a non-zero code if any selected checks fail. -``` -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`. +## Validation rules. +- `any` check: fails when API declarations use `any` (`TypeKind.AnyKind`). +- `comments` check: fails when descriptions are missing for API items. +- `exports` check: fails when public API items are missing from plugin exports discovered during analysis. +- Third-party code under `node_modules/` is ignored for validation. diff --git a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts index 2853e47323f89..d42ebb80461cf 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts @@ -61,12 +61,11 @@ export async function collectStats( allPluginStats[id] = { ...(await countEslintDisableLines(paths)), ...(await countEnzymeImports(paths)), - ...collectApiStatsForPlugin( - pluginApi, + ...collectApiStatsForPlugin(pluginApi, { missingApiItems, referencedDeprecations, - adoptionTrackedAPIs - ), + adoptionTrackedAPIs, + }), owner: plugin.manifest.owner, description: plugin.manifest.description, isPlugin: plugin.isPlugin, 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 a270695b0ad39..61f9408a27c4e 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 @@ -124,18 +124,16 @@ beforeAll(async () => { doc = pluginApiMap.pluginA; - pluginAStats = collectApiStatsForPlugin( - doc, + pluginAStats = collectApiStatsForPlugin(doc, { missingApiItems, referencedDeprecations, - adoptionTrackedAPIs - ); - pluginBStats = collectApiStatsForPlugin( - pluginApiMap.pluginB, + adoptionTrackedAPIs, + }); + pluginBStats = collectApiStatsForPlugin(pluginApiMap.pluginB, { missingApiItems, referencedDeprecations, - adoptionTrackedAPIs - ); + adoptionTrackedAPIs, + }); mdxOutputFolder = Path.resolve(__dirname, 'snapshots'); await Promise.all([ diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx index 22eba0f9b8c5d..66f913774e3ed 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA title: "pluginA" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginA plugin -date: 2025-12-31 +date: 2026-01-28 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA'] --- import pluginAObj from './plugin_a.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx index 0f89d454cf2ab..fc2fe59c5cfc2 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA-foo title: "pluginA.foo" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginA.foo plugin -date: 2025-12-31 +date: 2026-01-28 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo'] --- import pluginAFooObj from './plugin_a_foo.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx index b66ffe4301d1a..1cc0bcff34edf 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginB title: "pluginB" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginB plugin -date: 2025-12-31 +date: 2026-01-28 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginB'] --- import pluginBObj from './plugin_b.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/stats.test.ts b/packages/kbn-docs-utils/src/stats.test.ts index e4285b6a03dcf..b89aa2a493a7e 100644 --- a/packages/kbn-docs-utils/src/stats.test.ts +++ b/packages/kbn-docs-utils/src/stats.test.ts @@ -11,6 +11,7 @@ import type { AdoptionTrackedAPIsByPlugin, ApiDeclaration, ApiReference, + IssuesByPlugin, MissingApiItemMap, PluginApi, ReferencedDeprecationsByPlugin, @@ -18,6 +19,12 @@ import type { import { TypeKind } from './types'; import { collectApiStatsForPlugin } from './stats'; +const createEmptyIssues = (): IssuesByPlugin => ({ + missingApiItems: {}, + referencedDeprecations: {}, + adoptionTrackedAPIs: {}, +}); + const createMockApiDeclaration = (overrides: Partial = {}): ApiDeclaration => ({ id: 'test-id', label: 'test-label', @@ -48,7 +55,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(1); expect(stats.missingComments[0].id).toBe('no-comment'); @@ -65,7 +72,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(1); expect(stats.missingComments[0].id).toBe('empty-comment'); @@ -82,7 +89,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(0); }); @@ -110,7 +117,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(1); expect(stats.missingComments[0].id).toBe('child-no-comment'); @@ -141,7 +148,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(1); expect(stats.missingComments[0].id).toBe('level3-no-comment'); @@ -169,7 +176,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(3); expect(stats.missingComments.map((d) => d.id)).toEqual([ @@ -197,7 +204,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); // Should only flag the regular API, not the node_modules one expect(stats.missingComments).toHaveLength(1); @@ -217,7 +224,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.isAnyType).toHaveLength(1); expect(stats.isAnyType[0].id).toBe('any-type'); @@ -241,7 +248,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.isAnyType).toHaveLength(0); }); @@ -262,7 +269,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.isAnyType).toHaveLength(1); expect(stats.isAnyType[0].id).toBe('child-any'); @@ -290,7 +297,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.isAnyType).toHaveLength(3); }); @@ -307,7 +314,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems, + referencedDeprecations: {}, + adoptionTrackedAPIs: {}, + }); expect(stats.missingExports).toBe(2); }); @@ -315,7 +326,7 @@ describe('collectApiStatsForPlugin', () => { it('handles empty missingApiItems', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingExports).toBe(0); }); @@ -329,7 +340,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems, + referencedDeprecations: {}, + adoptionTrackedAPIs: {}, + }); expect(stats.missingExports).toBe(0); }); @@ -341,7 +356,7 @@ describe('collectApiStatsForPlugin', () => { client: [createMockApiDeclaration({ id: 'api1' })], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.apiCount).toBe(1); }); @@ -356,7 +371,7 @@ describe('collectApiStatsForPlugin', () => { common: [createMockApiDeclaration({ id: 'common1' })], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.apiCount).toBe(4); }); @@ -374,7 +389,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); // Parent + 2 children = 3 expect(stats.apiCount).toBe(3); @@ -395,7 +410,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); // level1 + level2 + level3 = 3 expect(stats.apiCount).toBe(3); @@ -417,7 +432,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.noReferences).toHaveLength(2); expect(stats.noReferences.map((d) => d.id)).toEqual(['no-refs', 'empty-refs']); @@ -435,7 +450,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.noReferences).toHaveLength(0); }); @@ -456,7 +471,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.noReferences).toHaveLength(1); expect(stats.noReferences[0].id).toBe('child-no-refs'); @@ -465,7 +480,7 @@ describe('collectApiStatsForPlugin', () => { describe('deprecation tracking', () => { it('counts referenced deprecations', () => { - const deprecations: ReferencedDeprecationsByPlugin = { + const referencedDeprecations: ReferencedDeprecationsByPlugin = { 'test-plugin': [ { deprecatedApi: createMockApiDeclaration({ id: 'deprecated1' }), @@ -480,7 +495,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, deprecations, {}); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems: {}, + referencedDeprecations, + adoptionTrackedAPIs: {}, + }); expect(stats.deprecatedAPIsReferencedCount).toBe(2); }); @@ -488,13 +507,13 @@ describe('collectApiStatsForPlugin', () => { it('handles no deprecations', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.deprecatedAPIsReferencedCount).toBe(0); }); it('handles missing plugin entry in deprecations', () => { - const deprecations: ReferencedDeprecationsByPlugin = { + const referencedDeprecations: ReferencedDeprecationsByPlugin = { 'other-plugin': [ { deprecatedApi: createMockApiDeclaration({ id: 'deprecated' }), @@ -505,7 +524,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, deprecations, {}); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems: {}, + referencedDeprecations, + adoptionTrackedAPIs: {}, + }); expect(stats.deprecatedAPIsReferencedCount).toBe(0); }); @@ -528,7 +551,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, adoptionTrackedAPIs); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems: {}, + referencedDeprecations: {}, + adoptionTrackedAPIs, + }); expect(stats.adoptionTrackedAPIs).toHaveLength(2); expect(stats.adoptionTrackedAPIsCount).toBe(2); @@ -538,7 +565,7 @@ describe('collectApiStatsForPlugin', () => { it('handles no adoption-tracked APIs', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.adoptionTrackedAPIs).toHaveLength(0); expect(stats.adoptionTrackedAPIsCount).toBe(0); @@ -557,7 +584,11 @@ describe('collectApiStatsForPlugin', () => { const pluginApi = createMockPluginApi(); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, adoptionTrackedAPIs); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems: {}, + referencedDeprecations: {}, + adoptionTrackedAPIs, + }); expect(stats.adoptionTrackedAPIs).toHaveLength(0); expect(stats.adoptionTrackedAPIsCount).toBe(0); @@ -591,7 +622,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); // Should flag the child property as missing comment // Note: This is current behavior - in Phase 4 we'll fix this to check for property-level JSDoc @@ -628,7 +659,7 @@ describe('collectApiStatsForPlugin', () => { ], }); - const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); expect(stats.missingComments).toHaveLength(1); expect(stats.missingComments[0].id).toBe('level3-no-comment'); @@ -678,7 +709,7 @@ describe('collectApiStatsForPlugin', () => { }, }; - const deprecations: ReferencedDeprecationsByPlugin = { + const referencedDeprecations: ReferencedDeprecationsByPlugin = { 'test-plugin': [ { deprecatedApi: createMockApiDeclaration({ id: 'deprecated' }), @@ -687,7 +718,11 @@ describe('collectApiStatsForPlugin', () => { ], }; - const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, deprecations, {}); + const stats = collectApiStatsForPlugin(pluginApi, { + missingApiItems, + referencedDeprecations, + adoptionTrackedAPIs: {}, + }); expect(stats.apiCount).toBe(5); expect(stats.missingComments).toHaveLength(1); diff --git a/packages/kbn-docs-utils/src/stats.ts b/packages/kbn-docs-utils/src/stats.ts index 7dd13b43f8ae6..4e78b605963fd 100644 --- a/packages/kbn-docs-utils/src/stats.ts +++ b/packages/kbn-docs-utils/src/stats.ts @@ -11,18 +11,17 @@ import { type AdoptionTrackedAPIsByPlugin, type ApiDeclaration, type ApiStats, - type MissingApiItemMap, + type IssuesByPlugin, type PluginApi, - type ReferencedDeprecationsByPlugin, TypeKind, } from './types'; -export function collectApiStatsForPlugin( - doc: PluginApi, - missingApiItems: MissingApiItemMap, - deprecations: ReferencedDeprecationsByPlugin, - adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin -): ApiStats { +/** + * Collects API stats for a single plugin. + */ +export function collectApiStatsForPlugin(doc: PluginApi, issues: IssuesByPlugin): ApiStats { + const { missingApiItems, referencedDeprecations, adoptionTrackedAPIs } = issues; + const stats: ApiStats = { missingComments: [], isAnyType: [], @@ -44,7 +43,9 @@ export function collectApiStatsForPlugin( Object.values(doc.common).forEach((def) => { collectStatsForApi(def, stats, doc); }); - stats.deprecatedAPIsReferencedCount = deprecations[doc.id] ? deprecations[doc.id].length : 0; + stats.deprecatedAPIsReferencedCount = referencedDeprecations[doc.id] + ? referencedDeprecations[doc.id].length + : 0; collectAdoptionTrackedAPIStats(doc, stats, adoptionTrackedAPIs); diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index e693f0b615ef0..1378cce34be1e 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -308,6 +308,16 @@ export interface ApiStats { adoptionTrackedAPIsUnreferencedCount: number; } +/** + * Collections of issues and metadata indexed by plugin ID. + * Used by `collectApiStatsForPlugin` to gather stats for a specific plugin. + */ +export interface IssuesByPlugin { + missingApiItems: MissingApiItemMap; + referencedDeprecations: ReferencedDeprecationsByPlugin; + adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin; +} + export type PluginMetaInfo = ApiStats & { owner: { name: string; githubTeam?: string }; description?: string;