Skip to content
Closed
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
9 changes: 6 additions & 3 deletions packages/kbn-docs-utils/src/build_api_docs_cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,13 @@ describe('build_api_docs_cli', () => {
});

it('runs build flow when stats are not provided', async () => {
const mockPlugins = [
{ id: 'p1', manifest: { owner: { name: 'team' }, serviceFolders: [] }, isPlugin: true },
];
const setupResult = {
project: {},
plugins: [
{ id: 'p1', manifest: { owner: { name: 'team' }, serviceFolders: [] }, isPlugin: true },
],
plugins: mockPlugins,
allPlugins: mockPlugins,
};
const apiMapResult = {
pluginApiMap: {},
Expand All @@ -107,6 +109,7 @@ describe('build_api_docs_cli', () => {
expect(buildApiMap).toHaveBeenCalledWith(
setupResult.project,
setupResult.plugins,
setupResult.allPlugins,
log,
mockTx,
{ stats: undefined, collectReferences: false }
Expand Down
4 changes: 4 additions & 0 deletions packages/kbn-docs-utils/src/build_api_docs_cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ async function endTransactionWithFailure(transaction: Transaction | null) {
}
}

/**
* Runs the build API docs CLI, generating API documentation for Kibana plugins and packages.
*/
export function runBuildApiDocsCli() {
startApm();
run(
Expand Down Expand Up @@ -85,6 +88,7 @@ export function runBuildApiDocsCli() {
const apiMapResult = buildApiMap(
setupResult.project,
setupResult.plugins,
setupResult.allPlugins,
log,
transaction,
options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('runCheckPackageDocs', () => {
expect(reportMetrics).toHaveBeenCalled();
expect(process.exitCode).toBe(1);
expect(log.error).toHaveBeenCalledWith(
expect.stringContaining('Validation failed for 1 plugin')
expect.stringContaining('Validation failed for 1 package')
);
expect(mockTx.end).toHaveBeenCalled();
});
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('runCheckPackageDocs', () => {
await runCheckPackageDocs(log as any, { plugin: 'plugin-a' } as any);

expect(process.exitCode).toBeUndefined();
expect(log.info).toHaveBeenCalledWith('All plugins passed validation.');
expect(log.info).toHaveBeenCalledWith('All packages passed validation.');
expect(mockTx.end).toHaveBeenCalled();
});
});
17 changes: 14 additions & 3 deletions packages/kbn-docs-utils/src/check_package_docs_cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from './cli';
import type { PluginOrPackage, MissingApiItemMap } from './types';
import type { AllPluginStats } from './cli/types';
import { writeFlatStatsFiles } from './cli/tasks/flat_stats';

type ValidationCheck = 'any' | 'comments' | 'exports' | 'unnamed';

Expand Down Expand Up @@ -129,6 +130,7 @@ export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => {
const apiMapResult = buildApiMap(
setupResult.project,
setupResult.plugins,
setupResult.allPlugins,
log,
transaction,
optionsWithChecks
Expand All @@ -142,6 +144,10 @@ export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => {
optionsWithChecks
);

if (optionsWithChecks.writeStats) {
writeFlatStatsFiles(setupResult.plugins, apiMapResult, allPluginStats);
}

reportMetrics(setupResult, apiMapResult, allPluginStats, log, transaction, {
...optionsWithChecks,
stats: checks,
Expand All @@ -160,13 +166,13 @@ export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => {

if (failingPlugins.length > 0) {
log.error(
`Validation failed for ${failingPlugins.length} plugin(s): ${failingPlugins
.map((plugin) => plugin.pluginId)
`Validation failed for ${failingPlugins.length} package(s): ${failingPlugins
.map(({ pluginId }) => pluginId)
.join(', ')}.`
);
process.exitCode = 1;
} else {
log.info('All plugins passed validation.');
log.info('All packages passed validation.');
}
} catch (error) {
transaction?.setOutcome('failure');
Expand All @@ -177,6 +183,9 @@ export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => {
}
};

/**
* Runs the check package docs CLI, validating API documentation for Kibana plugins and packages.
*/
export const runCheckPackageDocsCli = () => {
run(
async ({ log, flags }) => {
Expand All @@ -188,11 +197,13 @@ export const runCheckPackageDocsCli = () => {
},
flags: {
string: ['plugin', 'package', 'check'],
boolean: ['write'],
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.
--write Write stats to a flat JSON file in each plugin's target/api_docs/ directory.
`,
},
}
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-docs-utils/src/cli/parse_cli_flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const normalizeStats = (value: unknown | string[]) => {
*/
export function parseCliFlags(flags: CliFlags): CliOptions {
const collectReferences = flags.references === true;
const writeStats = flags.write === true;
const pluginFilter = dedupe(normalizeStringList(flags.plugin, 'plugin'));
const packageFilter = dedupe(normalizeStringList(flags.package, 'package'));
const rawStats = normalizeStats(flags.stats);
Expand All @@ -122,5 +123,6 @@ export function parseCliFlags(flags: CliFlags): CliOptions {
stats,
pluginFilter,
packageFilter,
writeStats,
};
}
9 changes: 5 additions & 4 deletions packages/kbn-docs-utils/src/cli/tasks/build_api_map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ describe('buildApiMap', () => {
pluginFilter: ['test-plugin'],
};

buildApiMap(project, plugins, log, transaction, options);
const allPlugins = [...plugins, { id: 'other-plugin' }];
buildApiMap(project, plugins, allPlugins, log, transaction, options);

expect(getPluginApiMap).toHaveBeenCalledWith(project, plugins, log, {
expect(getPluginApiMap).toHaveBeenCalledWith(project, plugins, allPlugins, log, {
collectReferences: true,
pluginFilter: ['test-plugin'],
});
Expand All @@ -80,7 +81,7 @@ describe('buildApiMap', () => {
collectReferences: false,
};

const result = buildApiMap(project, plugins, log, transaction, options);
const result = buildApiMap(project, plugins, plugins, log, transaction, options);

expect(result).toBeDefined();
expect(result.pluginApiMap).toBeDefined();
Expand All @@ -95,7 +96,7 @@ describe('buildApiMap', () => {
collectReferences: false,
};

buildApiMap(project, plugins, log, transaction, options);
buildApiMap(project, plugins, plugins, log, transaction, options);

expect(transaction.startSpan).toHaveBeenCalledWith('build_api_docs.getPluginApiMap', 'setup');
});
Expand Down
4 changes: 3 additions & 1 deletion packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { BuildApiMapResult, CliOptions } from '../types';
*
* @param project - TypeScript project instance.
* @param plugins - List of plugins and packages to analyze.
* @param allPlugins - All plugins/packages for cross-reference resolution.
* @param log - Tooling log instance.
* @param transaction - APM transaction for tracking.
* @param options - CLI options including collectReferences and pluginFilter.
Expand All @@ -33,6 +34,7 @@ import type { BuildApiMapResult, CliOptions } from '../types';
export function buildApiMap(
project: Project,
plugins: PluginOrPackage[],
allPlugins: PluginOrPackage[],
log: ToolingLog,
transaction: Transaction,
options: CliOptions
Expand All @@ -46,7 +48,7 @@ export function buildApiMap(
referencedDeprecations,
adoptionTrackedAPIs,
unnamedExports,
} = getPluginApiMap(project, plugins, log, {
} = getPluginApiMap(project, plugins, allPlugins, log, {
collectReferences: options.collectReferences,
pluginFilter: options.pluginFilter,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('collectStats', () => {
plugins: [mockPlugin],
pathsByPlugin: new Map([[mockPlugin, ['src/plugins/test/public/index.ts']]]),
project: {} as any,
allPlugins: [mockPlugin],
};

apiMapResult = {
Expand Down
117 changes: 117 additions & 0 deletions packages/kbn-docs-utils/src/cli/tasks/flat_stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 fs from 'fs';
import Path from 'path';

import type { ApiDeclaration, MissingApiItemMap, PluginOrPackage } from '../../types';
import type { AllPluginStats, BuildApiMapResult } from '../types';
import { getLink } from './get_link';

/** Shape of a single stat entry in the flat JSON output. */
export interface FlatStatEntry {
id: string;
label: string;
path: string;
type: string;
lineNumber?: number;
columnNumber?: number;
link: string;
}

/** Shape of a missing-export entry in the flat JSON output. */
export interface FlatMissingExportEntry {
source: string;
references: string[];
}

/** Complete flat stats JSON written per plugin/package. */
export interface FlatStats {
counts: {
apiCount: number;
missingExports: number;
missingComments: number;
isAnyType: number;
noReferences: number;
missingReturns: number;
paramDocMismatches: number;
missingComplexTypeInfo: number;
};
missingComments: FlatStatEntry[];
isAnyType: FlatStatEntry[];
noReferences: FlatStatEntry[];
missingReturns: FlatStatEntry[];
paramDocMismatches: FlatStatEntry[];
missingComplexTypeInfo: FlatStatEntry[];
missingExports: FlatMissingExportEntry[];
}

const mapStat = (dec: ApiDeclaration): FlatStatEntry => ({
id: dec.id,
label: dec.label,
path: dec.path,
type: dec.type,
lineNumber: dec.lineNumber,
columnNumber: dec.columnNumber,
link: getLink(dec),
});

export const buildFlatStatsForPlugin = (
pluginId: string,
pluginStats: AllPluginStats[string],
missingApiItems: MissingApiItemMap
): FlatStats => {
const missingExportsCount = missingApiItems[pluginId]
? Object.keys(missingApiItems[pluginId]).length
: 0;
const missingExportsList = missingApiItems[pluginId]
? Object.keys(missingApiItems[pluginId]).map((source) => ({
source,
references: missingApiItems[pluginId][source],
}))
: [];

return {
counts: {
apiCount: pluginStats.apiCount,
missingExports: missingExportsCount,
missingComments: pluginStats.missingComments.length,
isAnyType: pluginStats.isAnyType.length,
noReferences: pluginStats.noReferences.length,
missingReturns: pluginStats.missingReturns.length,
paramDocMismatches: pluginStats.paramDocMismatches.length,
missingComplexTypeInfo: pluginStats.missingComplexTypeInfo.length,
},
missingComments: pluginStats.missingComments.map(mapStat),
isAnyType: pluginStats.isAnyType.map(mapStat),
noReferences: pluginStats.noReferences.map(mapStat),
missingReturns: pluginStats.missingReturns.map(mapStat),
paramDocMismatches: pluginStats.paramDocMismatches.map(mapStat),
missingComplexTypeInfo: pluginStats.missingComplexTypeInfo.map(mapStat),
missingExports: missingExportsList,
};
};

export const writeFlatStatsFiles = (
plugins: PluginOrPackage[],
apiMapResult: BuildApiMapResult,
allPluginStats: AllPluginStats
) => {
for (const plugin of plugins) {
const stats = allPluginStats[plugin.id];
if (!stats) {
continue;
}
const flat = buildFlatStatsForPlugin(plugin.id, stats, apiMapResult.missingApiItems);
const pluginTargetDir = Path.resolve(plugin.directory, 'target', 'api_docs');
fs.mkdirSync(pluginTargetDir, { recursive: true });
const target = Path.join(pluginTargetDir, 'stats.json');
fs.writeFileSync(target, JSON.stringify(flat, null, 2));
}
};
32 changes: 32 additions & 0 deletions packages/kbn-docs-utils/src/cli/tasks/get_link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { ApiDeclaration } from '../../types';

/**
* Generates a link to the GitHub source for an API declaration.
*
* When a `lineNumber` is available, produces a direct `#L42`-style anchor.
* Otherwise falls back to a text-fragment search (`#:~:text=...`).
*
* TODO: clintandrewhall - allow `base` to be overridden in the instance of a CI build
* associated with a PR.
*
* @param declaration - API declaration to generate link for.
* @returns GitHub link to the source code.
*/
export const getLink = (declaration: ApiDeclaration): string => {
const base = `https://github.com/elastic/kibana/blob/main/${declaration.path}`;
if (declaration.lineNumber) {
return `${base}#L${declaration.lineNumber}`;
}
return `https://github.com/elastic/kibana/tree/main/${
declaration.path
}#:~:text=${encodeURIComponent(declaration.label)}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('reportMetrics', () => {
plugins: [mockPlugin],
pathsByPlugin: new Map(),
project: {} as any,
allPlugins: [mockPlugin],
};

apiMapResult = {
Expand Down
Loading