Skip to content
451 changes: 42 additions & 409 deletions packages/kbn-docs-utils/src/build_api_docs_cli.ts

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions packages/kbn-docs-utils/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { parseCliFlags } from './parse_cli_flags';
export type { CliFlags, CliOptions, CliContext } from './types';
export { setupProject, buildApiMap, collectStats, writeDocs, reportMetrics } from './tasks';
113 changes: 113 additions & 0 deletions packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts
Original file line number Diff line number Diff line change
@@ -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 { parseCliFlags } from './parse_cli_flags';
import type { CliFlags } from './types';

describe('parseCliFlags', () => {
it('parses valid flags correctly', () => {
const flags: CliFlags = {
references: true,
stats: ['any', 'comments'],
plugin: ['plugin1', 'plugin2'],
};

const result = parseCliFlags(flags);

expect(result.collectReferences).toBe(true);
expect(result.stats).toEqual(['any', 'comments']);
expect(result.pluginFilter).toEqual(['plugin1', 'plugin2']);
});

it('normalizes single string plugin to array', () => {
const flags: CliFlags = {
plugin: 'single-plugin',
};

const result = parseCliFlags(flags);

expect(result.pluginFilter).toEqual(['single-plugin']);
});

it('normalizes single string stats to array', () => {
const flags: CliFlags = {
stats: 'any',
};

const result = parseCliFlags(flags);

expect(result.stats).toEqual(['any']);
});

it('handles undefined flags', () => {
const flags: CliFlags = {};

const result = parseCliFlags(flags);

expect(result.collectReferences).toBe(false);
expect(result.stats).toBeUndefined();
expect(result.pluginFilter).toBeUndefined();
});

it('throws error for invalid plugin filter type', () => {
const flags: CliFlags = {
plugin: { invalid: 'object' } as any,
};

expect(() => parseCliFlags(flags)).toThrow('expected --plugin must only contain strings');
});

it('throws error for invalid stats values', () => {
const flags: CliFlags = {
stats: ['invalid-value'],
};

expect(() => parseCliFlags(flags)).toThrow(
'expected --stats must only contain `any`, `comments` and/or `exports`'
);
});

it('throws error for invalid stats type', () => {
const flags: CliFlags = {
stats: { invalid: 'object' } as any,
};

expect(() => parseCliFlags(flags)).toThrow(
'expected --stats must only contain `any`, `comments` and/or `exports`'
);
});

it('accepts valid stats values', () => {
const flags: CliFlags = {
stats: ['any', 'comments', 'exports'],
};

const result = parseCliFlags(flags);

expect(result.stats).toEqual(['any', 'comments', 'exports']);
});

it('handles references flag correctly', () => {
const flags: CliFlags = {
references: true,
};

const result = parseCliFlags(flags);

expect(result.collectReferences).toBe(true);
});

it('handles references flag as false when not provided', () => {
const flags: CliFlags = {};

const result = parseCliFlags(flags);

expect(result.collectReferences).toBe(false);
});
});
53 changes: 53 additions & 0 deletions packages/kbn-docs-utils/src/cli/parse_cli_flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { createFlagError } from '@kbn/dev-cli-errors';
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');
}

/**
* Parses and validates CLI flags, normalizing them into a consistent format.
*
* @param flags - Raw flags from the CLI runner.
* @returns Validated and normalized CLI options.
* @throws {Error} If flags are invalid.
*/
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`');
}

return {
collectReferences,
stats: stats && isStringArray(stats) ? stats : undefined,
pluginFilter,
};
}
102 changes: 102 additions & 0 deletions packages/kbn-docs-utils/src/cli/tasks/build_api_map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 { Project } from 'ts-morph';
import { ToolingLog } from '@kbn/tooling-log';
import { buildApiMap } from './build_api_map';
import type { CliOptions } from '../types';

// Mock getPluginApiMap
jest.mock('../../get_plugin_api_map', () => ({
getPluginApiMap: jest.fn(() => ({
pluginApiMap: {},
missingApiItems: {},
referencedDeprecations: {},
unreferencedDeprecations: {},
adoptionTrackedAPIs: {},
})),
}));

import { getPluginApiMap } from '../../get_plugin_api_map';

describe('buildApiMap', () => {
let project: Project;
let log: ToolingLog;
let transaction: any;
let plugins: any[];

beforeEach(() => {
project = new Project({
useInMemoryFileSystem: true,
});

log = new ToolingLog({
level: 'silent',
writeTo: process.stdout,
});

transaction = {
startSpan: jest.fn(() => ({
end: jest.fn(),
})),
};

plugins = [
{
id: 'test-plugin',
directory: 'src/plugins/test',
isPlugin: true,
manifest: {
id: 'test-plugin',
owner: { name: 'test-team' },
serviceFolders: [],
},
},
];
});

it('calls getPluginApiMap with correct parameters', () => {
const options: CliOptions = {
collectReferences: true,
pluginFilter: ['test-plugin'],
};

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

expect(getPluginApiMap).toHaveBeenCalledWith(project, plugins, log, {
collectReferences: true,
pluginFilter: ['test-plugin'],
});
});

it('returns result from getPluginApiMap', () => {
const options: CliOptions = {
collectReferences: false,
};

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

expect(result).toBeDefined();
expect(result.pluginApiMap).toBeDefined();
expect(result.missingApiItems).toBeDefined();
expect(result.referencedDeprecations).toBeDefined();
expect(result.unreferencedDeprecations).toBeDefined();
expect(result.adoptionTrackedAPIs).toBeDefined();
});

it('creates APM span for tracking', () => {
const options: CliOptions = {
collectReferences: false,
};

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

expect(transaction.startSpan).toHaveBeenCalledWith('build_api_docs.getPluginApiMap', 'setup');
});
});
62 changes: 62 additions & 0 deletions packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { Transaction } from 'elastic-apm-node';
import type { Project } from 'ts-morph';
import type { ToolingLog } from '@kbn/tooling-log';
import { getPluginApiMap } from '../../get_plugin_api_map';
import type { PluginOrPackage } from '../../types';
import type { BuildApiMapResult, CliOptions } from '../types';

/**
* Builds the plugin API map by analyzing TypeScript code.
*
* This task:
* - Analyzes TypeScript source files using the project
* - Extracts API declarations from plugins
* - Collects missing API items, deprecations, and adoption-tracked APIs
* - Optionally collects references between APIs
*
* @param project - TypeScript project instance.
* @param plugins - List of plugins and packages to analyze.
* @param log - Tooling log instance.
* @param transaction - APM transaction for tracking.
* @param options - CLI options including collectReferences and pluginFilter.
* @returns Built API map with all collected metadata.
*/
export function buildApiMap(
project: Project,
plugins: PluginOrPackage[],
log: ToolingLog,
transaction: Transaction,
options: CliOptions
): BuildApiMapResult {
const spanPluginApiMap = transaction.startSpan('build_api_docs.getPluginApiMap', 'setup');

const {
pluginApiMap,
missingApiItems,
unreferencedDeprecations,
referencedDeprecations,
adoptionTrackedAPIs,
} = getPluginApiMap(project, plugins, log, {
collectReferences: options.collectReferences,
pluginFilter: options.pluginFilter,
});

spanPluginApiMap?.end();

return {
pluginApiMap,
missingApiItems,
referencedDeprecations,
unreferencedDeprecations,
adoptionTrackedAPIs,
};
}
Loading