in packages/kbn-docs-utils/src/build_api_docs_cli.ts [53:444]
export function runBuildApiDocsCli() {
run(
async ({ log, flags }) => {
const transaction = apm.startTransaction('build-api-docs', 'kibana-cli');
const spanSetup = transaction.startSpan('build_api_docs.setup', 'setup');
const collectReferences = flags.references as boolean;
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)) {
await endTransactionWithFailure(transaction);
throw createFlagError('expected --plugin must only contain strings');
}
if (
(stats &&
isStringArray(stats) &&
stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) ||
(stats && !isStringArray(stats))
) {
await endTransactionWithFailure(transaction);
throw createFlagError(
'expected --stats must only contain `any`, `comments` and/or `exports`'
);
}
const outputFolder = Path.resolve(REPO_ROOT, 'api_docs');
spanSetup?.end();
const spanInitialDocIds = transaction.startSpan('build_api_docs.initialDocIds', 'setup');
const initialDocIds =
!pluginFilter && Fs.existsSync(outputFolder)
? await getAllDocFileIds(outputFolder)
: undefined;
spanInitialDocIds?.end();
const spanPlugins = transaction.startSpan('build_api_docs.findPlugins', 'setup');
const plugins = findPlugins(stats && pluginFilter ? pluginFilter : undefined);
if (stats && Array.isArray(pluginFilter) && pluginFilter.length !== plugins.length) {
await endTransactionWithFailure(transaction);
throw createFlagError('expected --plugin was not found');
}
spanPlugins?.end();
const spanPathsByPackage = transaction.startSpan('build_api_docs.getPathsByPackage', 'setup');
const pathsByPlugin = await getPathsByPackage(plugins);
spanPathsByPackage?.end();
const spanProject = transaction.startSpan('build_api_docs.getTsProject', 'setup');
const project = getTsProject(
REPO_ROOT,
stats && pluginFilter && plugins.length === 1 ? plugins[0].directory : undefined
);
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) && !pluginFilter) {
await Fsp.rm(outputFolder, { recursive: true });
}
// if the output folder doesn't exist, create it
if (!Fs.existsSync(outputFolder)) {
await Fsp.mkdir(outputFolder, { recursive: true });
}
spanFolders?.end();
const spanPluginApiMap = transaction.startSpan('build_api_docs.getPluginApiMap', 'setup');
const {
pluginApiMap,
missingApiItems,
unreferencedDeprecations,
referencedDeprecations,
adoptionTrackedAPIs,
} = getPluginApiMap(project, plugins, log, { collectReferences, pluginFilter });
spanPluginApiMap?.end();
const reporter = CiStatsReporter.fromEnv(log);
const allPluginStats: { [key: string]: PluginMetaInfo & ApiStats & EslintDisableCounts } = {};
for (const plugin of plugins) {
const id = plugin.id;
if (stats && pluginFilter && !pluginFilter.includes(plugin.id)) {
continue;
}
const spanApiStatsForPlugin = transaction.startSpan(
`build_api_docs.collectApiStatsForPlugin-${id}`,
'stats'
);
const pluginApi = pluginApiMap[id];
const paths = pathsByPlugin.get(plugin) ?? [];
allPluginStats[id] = {
...(await countEslintDisableLines(paths)),
...collectApiStatsForPlugin(
pluginApi,
missingApiItems,
referencedDeprecations,
adoptionTrackedAPIs
),
owner: plugin.manifest.owner,
description: plugin.manifest.description,
isPlugin: plugin.isPlugin,
};
spanApiStatsForPlugin?.end();
}
if (!stats) {
const spanWritePluginDirectoryDoc = transaction.startSpan(
'build_api_docs.writePluginDirectoryDoc',
'write'
);
await writePluginDirectoryDoc(outputFolder, pluginApiMap, allPluginStats, log);
spanWritePluginDirectoryDoc?.end();
}
for (const plugin of plugins) {
// Note that the filtering is done here, and not above because the entire public plugin API has to
// be parsed in order to correctly determine reference links, and ensure that `removeBrokenLinks`
// doesn't remove more links than necessary.
if (pluginFilter && !pluginFilter.includes(plugin.id)) {
continue;
}
const id = plugin.id;
const pluginApi = pluginApiMap[id];
const pluginStats = allPluginStats[id];
const pluginTeam = plugin.manifest.owner.name;
const spanMetrics = transaction.startSpan(
`build_api_docs.collectApiStatsForPlugin-${id}`,
'stats'
);
reporter.metrics([
{
id,
meta: { pluginTeam },
group: 'Unreferenced deprecated APIs',
value: unreferencedDeprecations[id] ? unreferencedDeprecations[id].length : 0,
},
{
id,
meta: { pluginTeam },
group: 'API count',
value: pluginStats.apiCount,
},
{
id,
meta: { pluginTeam },
group: 'API count missing comments',
value: pluginStats.missingComments.length,
},
{
id,
meta: { pluginTeam },
group: 'API count with any type',
value: pluginStats.isAnyType.length,
},
{
id,
meta: { pluginTeam },
group: 'Non-exported public API item count',
value: missingApiItems[id] ? Object.keys(missingApiItems[id]).length : 0,
},
{
id,
meta: { pluginTeam },
group: 'References to deprecated APIs',
value: pluginStats.deprecatedAPIsReferencedCount,
},
{
id,
meta: {
pluginTeam,
// `meta` only allows primitives or string[]
// Also, each string is allowed to have a max length of 2056,
// so it's safer to stringify each element in the array over sending the entire array as stringified.
// My internal tests with 4 plugins using the same API gets to a length of 156 chars,
// so we should have enough room for tracking popular APIs.
// TODO: We can do a follow-up improvement to split the report if we find out we might hit the limit.
adoptionTrackedAPIs: pluginStats.adoptionTrackedAPIs.map((metric) =>
JSON.stringify(metric)
),
},
group: 'Adoption-tracked APIs',
value: pluginStats.adoptionTrackedAPIsCount,
},
{
id,
meta: { pluginTeam },
group: 'Adoption-tracked APIs that are not used anywhere',
value: pluginStats.adoptionTrackedAPIsUnreferencedCount,
},
{
id,
meta: { pluginTeam },
group: 'ESLint disabled line counts',
value: pluginStats.eslintDisableLineCount,
},
{
id,
meta: { pluginTeam },
group: 'ESLint disabled in files',
value: pluginStats.eslintDisableFileCount,
},
{
id,
meta: { pluginTeam },
group: 'Total ESLint disabled count',
value: pluginStats.eslintDisableFileCount + pluginStats.eslintDisableLineCount,
},
]);
const getLink = (d: ApiDeclaration) =>
`https://github.com/elastic/kibana/tree/main/${d.path}#:~:text=${encodeURIComponent(
d.label
)}`;
if (collectReferences && pluginFilter?.includes(plugin.id)) {
if (referencedDeprecations[id] && pluginStats.deprecatedAPIsReferencedCount > 0) {
log.info(`${referencedDeprecations[id].length} deprecated APIs used`);
// eslint-disable-next-line no-console
console.table(referencedDeprecations[id]);
} else {
log.info(`No referenced deprecations for plugin ${plugin.id}`);
}
if (pluginStats.noReferences.length > 0) {
// eslint-disable-next-line no-console
console.table(
pluginStats.noReferences.map((d) => ({
id: d.id,
link: getLink(d),
}))
);
} else {
log.info(`No unused APIs for plugin ${plugin.id}`);
}
}
if (stats) {
const passesAllChecks =
pluginStats.isAnyType.length === 0 &&
pluginStats.missingComments.length === 0 &&
pluginStats.deprecatedAPIsReferencedCount === 0 &&
(!missingApiItems[id] || Object.keys(missingApiItems[id]).length === 0);
log.info(`--- Plugin '${id}' ${passesAllChecks ? ` passes all checks ----` : '----`'}`);
if (!passesAllChecks) {
log.info(`${pluginStats.isAnyType.length} API items with ANY`);
if (stats.includes('any')) {
// eslint-disable-next-line no-console
console.table(
pluginStats.isAnyType.map((d) => ({
id: d.id,
link: getLink(d),
}))
);
}
log.info(`${pluginStats.missingComments.length} API items missing comments`);
if (stats.includes('comments')) {
// eslint-disable-next-line no-console
console.table(
pluginStats.missingComments.map((d) => ({
id: d.id,
link: getLink(d),
}))
);
}
if (missingApiItems[id]) {
log.info(
`${Object.keys(missingApiItems[id]).length} referenced API items not exported`
);
if (stats.includes('exports')) {
// eslint-disable-next-line no-console
console.table(
Object.keys(missingApiItems[id]).map((key) => ({
'Not exported source': key,
references: missingApiItems[id][key].join(', '),
}))
);
}
}
}
}
spanMetrics?.end();
if (!stats) {
if (pluginStats.apiCount > 0) {
log.info(`Writing public API doc for plugin ${pluginApi.id}.`);
const spanWritePluginDocs = transaction.startSpan(
'build_api_docs.writePluginDocs',
'write'
);
await writePluginDocs(outputFolder, { doc: pluginApi, plugin, pluginStats, log });
spanWritePluginDocs?.end();
} else {
log.info(`Plugin ${pluginApi.id} has no public API.`);
}
const spanWriteDeprecationDocByPlugin = transaction.startSpan(
'build_api_docs.writeDeprecationDocByPlugin',
'write'
);
await writeDeprecationDocByPlugin(outputFolder, referencedDeprecations, log);
spanWriteDeprecationDocByPlugin?.end();
const spanWriteDeprecationDueByTeam = transaction.startSpan(
'build_api_docs.writeDeprecationDueByTeam',
'write'
);
await writeDeprecationDueByTeam(outputFolder, referencedDeprecations, plugins, log);
spanWriteDeprecationDueByTeam?.end();
const spanWriteDeprecationDocByApi = transaction.startSpan(
'build_api_docs.writeDeprecationDocByApi',
'write'
);
await writeDeprecationDocByApi(
outputFolder,
referencedDeprecations,
unreferencedDeprecations,
log
);
spanWriteDeprecationDocByApi?.end();
}
}
if (Object.values(pathsOutsideScopes).length > 0) {
log.warning(`Found paths outside of normal scope folders:`);
log.warning(pathsOutsideScopes);
}
if (initialDocIds) {
await trimDeletedDocsFromNav(log, initialDocIds, outputFolder);
}
transaction.end();
},
{
log: {
defaultLevel: 'info',
},
flags: {
string: ['plugin', '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
`,
},
}
);
}