Automatically generated Api documentation (#86232)

* auto generated mdx api doc system

* Fix README.md

* update core api docs after master merge

* clean up signature

* Update packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts

Co-authored-by: Spencer <email@spalger.com>

* migrate to docs-util package

* Remove bad links

* fix ref to release-notes and add extra dats service folder

* update name change

* Update packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_type_kind.ts

Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com>

* Update packages/kbn-docs-utils/src/api_docs/utils.ts

Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com>

* review updates 1

* review feedback updates round 1

* Small refactor of extractImportReferences

* Review feedback updates 2

* Review update 3 plus support for links in class interface heritage clause

* debug failing test on ci only

* Escape regex directory path

* Update packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts

Co-authored-by: Spencer <email@spalger.com>

* fix for commit suggestion

Co-authored-by: Spencer <email@spalger.com>
Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com>
Co-authored-by: kobelb <brandon.kobel@elastic.co>
This commit is contained in:
Stacey Gammon 2021-02-24 19:23:19 -05:00 committed by GitHub
parent 91d03f0c45
commit deb555a552
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 4156 additions and 620 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.aws-config.json
.signing-config.json
/api_docs
.ackrc
/.es
/.chromium

View file

@ -28,6 +28,7 @@ Should never be used in code outside of Core but is exported for documentation p
| [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | <code>readonly string[]</code> | List of plugin ids that this plugin's UI code imports modules from that are not in <code>requiredPlugins</code>. |
| [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | <code>readonly PluginName[]</code> | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. |
| [server](./kibana-plugin-core-server.pluginmanifest.server.md) | <code>boolean</code> | Specifies whether plugin includes some server-side specific functionality. |
| [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | <code>readonly string[]</code> | Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. |
| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | <code>boolean</code> | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via <code>public/ui_plugin.js</code> file. |
| [version](./kibana-plugin-core-server.pluginmanifest.version.md) | <code>string</code> | Version of the plugin. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) &gt; [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md)
## PluginManifest.serviceFolders property
Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections.
<b>Signature:</b>
```typescript
readonly serviceFolders?: readonly string[];
```

View file

@ -47,6 +47,7 @@
"test:ftr:runner": "node scripts/functional_test_runner",
"checkLicenses": "node scripts/check_licenses --dev",
"build": "node scripts/build --all-platforms",
"build:apidocs": "node scripts/build_api_docs",
"start": "node scripts/kibana --dev",
"debug": "node --nolazy --inspect scripts/kibana --dev",
"debug-break": "node --nolazy --inspect-brk scripts/kibana --dev",
@ -367,7 +368,7 @@
"@kbn/plugin-generator": "link:packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers",
"@kbn/pm": "link:packages/kbn-pm",
"@kbn/release-notes": "link:packages/kbn-release-notes",
"@kbn/docs-utils": "link:packages/kbn-docs-utils",
"@kbn/storybook": "link:packages/kbn-storybook",
"@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools",
"@kbn/test": "link:packages/kbn-test",
@ -821,6 +822,7 @@
"tinycolor2": "1.4.1",
"topojson-client": "3.0.0",
"ts-loader": "^7.0.5",
"ts-morph": "^9.1.0",
"tsd": "^0.13.1",
"typescript": "4.1.3",
"typescript-fsa": "^3.0.0",

View file

@ -29,6 +29,7 @@ interface Manifest {
server: boolean;
kibanaVersion: string;
version: string;
serviceFolders: readonly string[];
requiredPlugins: readonly string[];
optionalPlugins: readonly string[];
requiredBundles: readonly string[];
@ -64,6 +65,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP
id: manifest.id,
version: manifest.version,
kibanaVersion: manifest.kibanaVersion || manifest.version,
serviceFolders: manifest.serviceFolders || [],
requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'),
optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'),
requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'),

View file

@ -9,5 +9,5 @@
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-release-notes'],
roots: ['<rootDir>/packages/kbn-docs-utils'],
};

View file

@ -1,5 +1,5 @@
{
"name": "@kbn/release-notes",
"name": "@kbn/docs-utils",
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"private": "true",

View file

@ -0,0 +1,12 @@
# Autogenerated API documentation
[RFC](../../../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 burder it will incur.
To generate the docs run
```
node scripts/build_api_docs
```

View file

@ -0,0 +1,85 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { Project, Node } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { TypeKind, ApiScope } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_plugin';
import { buildApiDeclaration } from './build_api_declaration';
import { isNamedNode } from '../tsmorph_utils';
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
let nodes: Node[];
let plugins: KibanaPlatformPlugin[];
function getNodeName(node: Node): string {
return isNamedNode(node) ? node.getName() : '';
}
beforeAll(() => {
const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json');
const project = new Project({
tsConfigFilePath,
});
plugins = [getKibanaPlatformPlugin('pluginA')];
nodes = getDeclarationNodesForPluginScope(project, plugins[0], ApiScope.CLIENT, log);
});
it('Test number primitive doc def', () => {
const node = nodes.find((n) => getNodeName(n) === 'aNum');
expect(node).toBeDefined();
const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT);
expect(def.type).toBe(TypeKind.NumberKind);
});
it('Function type is exported as type with signature', () => {
const node = nodes.find((n) => getNodeName(n) === 'FnWithGeneric');
expect(node).toBeDefined();
const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT);
expect(def).toBeDefined();
expect(def?.type).toBe(TypeKind.TypeKind);
expect(def?.signature?.length).toBeGreaterThan(0);
});
it('Test Interface Kind doc def', () => {
const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface');
expect(node).toBeDefined();
const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT);
expect(def.type).toBe(TypeKind.InterfaceKind);
expect(def.children).toBeDefined();
expect(def.children!.length).toBe(3);
});
it('Test union export', () => {
const node = nodes.find((n) => getNodeName(n) === 'aUnionProperty');
expect(node).toBeDefined();
const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT);
expect(def.type).toBe(TypeKind.CompoundTypeKind);
});
it('Function inside interface has a label', () => {
const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface');
expect(node).toBeDefined();
const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT);
const fn = def!.children?.find((c) => c.label === 'aFn');
expect(fn).toBeDefined();
expect(fn?.label).toBe('aFn');
expect(fn?.type).toBe(TypeKind.FunctionKind);
});

View file

@ -0,0 +1,88 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { Node } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { buildClassDec } from './build_class_dec';
import { buildFunctionDec } from './build_function_dec';
import { getCommentsFromNode } from './js_doc_utils';
import { isNamedNode } from '../tsmorph_utils';
import { AnchorLink, ApiDeclaration } from '../types';
import { buildVariableDec } from './build_variable_dec';
import { getApiSectionId } from '../utils';
import { getSourceForNode } from './utils';
import { buildTypeLiteralDec } from './build_type_literal_dec';
import { ApiScope } from '../types';
import { getSignature } from './get_signature';
import { buildInterfaceDec } from './build_interface_dec';
import { getTypeKind } from './get_type_kind';
/**
* A potentially recursive function, depending on the node type, that builds a JSON like structure
* that can be passed to the elastic-docs component for rendering as an API. Nodes like classes,
* interfaces, objects and functions will have children for their properties, members and parameters.
*
* @param node The ts-morph node to build an ApiDeclaration for.
* @param plugins The list of plugins registered is used for building cross plugin links by looking up
* the plugin by import path. We could accomplish the same thing via a regex on the import path, but this lets us
* decouple plugin path from plugin id.
* @param log Logs messages to console.
* @param pluginName The name of the plugin this declaration belongs to.
* @param scope The scope this declaration belongs to (server, public, or common).
* @param parentApiId If this declaration is nested inside another declaration, it should have a parent id. This
* is used to create the anchor link to this API item.
* @param name An optional name to pass through which will be used instead of node.getName, if it
* exists. For some types, like Parameters, the name comes on the parent node, but we want the doc def
* to be built from the TypedNode
*/
export function buildApiDeclaration(
node: Node,
plugins: KibanaPlatformPlugin[],
log: ToolingLog,
pluginName: string,
scope: ApiScope,
parentApiId?: string,
name?: string
): ApiDeclaration {
const apiName = name ? name : isNamedNode(node) ? node.getName() : 'Unnamed';
log.debug(`Building API Declaration for ${apiName} of kind ${node.getKindName()}`);
const apiId = parentApiId ? parentApiId + '.' + apiName : apiName;
const anchorLink: AnchorLink = { scope, pluginName, apiName: apiId };
if (Node.isClassDeclaration(node)) {
return buildClassDec(node, plugins, anchorLink, log);
} else if (Node.isInterfaceDeclaration(node)) {
return buildInterfaceDec(node, plugins, anchorLink, log);
} else if (
Node.isMethodSignature(node) ||
Node.isFunctionDeclaration(node) ||
Node.isMethodDeclaration(node) ||
Node.isConstructorDeclaration(node)
) {
return buildFunctionDec(node, plugins, anchorLink, log);
} else if (
Node.isPropertySignature(node) ||
Node.isPropertyDeclaration(node) ||
Node.isShorthandPropertyAssignment(node) ||
Node.isPropertyAssignment(node) ||
Node.isVariableDeclaration(node)
) {
return buildVariableDec(node, plugins, anchorLink, log);
} else if (Node.isTypeLiteralNode(node)) {
return buildTypeLiteralDec(node, plugins, anchorLink, log, apiName);
}
return {
id: getApiSectionId(anchorLink),
type: getTypeKind(node),
label: apiName,
description: getCommentsFromNode(node),
source: getSourceForNode(node),
signature: getSignature(node, plugins, log),
};
}

View file

@ -0,0 +1,63 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import {
ArrowFunction,
VariableDeclaration,
PropertyDeclaration,
PropertySignature,
ShorthandPropertyAssignment,
PropertyAssignment,
} from 'ts-morph';
import { getApiSectionId } from '../utils';
import { getCommentsFromNode } from './js_doc_utils';
import { AnchorLink, TypeKind } from '../types';
import { getSourceForNode } from './utils';
import { buildApiDecsForParameters } from './build_parameter_decs';
import { getSignature } from './get_signature';
import { getJSDocReturnTagComment } from './js_doc_utils';
/**
* Arrow functions are handled differently than regular functions because you need the arrow function
* initializer as well as the node. The initializer is where the parameters are grabbed from and the
* signature, while the node has the comments and name.
*
* @param node
* @param initializer
* @param plugins
* @param anchorLink
* @param log
*/
export function getArrowFunctionDec(
node:
| VariableDeclaration
| PropertyDeclaration
| PropertySignature
| ShorthandPropertyAssignment
| PropertyAssignment,
initializer: ArrowFunction,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog
) {
log.debug(
`Getting Arrow Function doc def for node ${node.getName()} of kind ${node.getKindName()}`
);
return {
id: getApiSectionId(anchorLink),
type: TypeKind.FunctionKind,
children: buildApiDecsForParameters(initializer.getParameters(), plugins, anchorLink, log),
signature: getSignature(initializer, plugins, log),
description: getCommentsFromNode(node),
label: node.getName(),
source: getSourceForNode(node),
returnComment: getJSDocReturnTagComment(node),
};
}

View file

@ -0,0 +1,47 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { ClassDeclaration } from 'ts-morph';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { getCommentsFromNode } from './js_doc_utils';
import { buildApiDeclaration } from './build_api_declaration';
import { getSourceForNode, isPrivate } from './utils';
import { getApiSectionId } from '../utils';
import { getSignature } from './get_signature';
export function buildClassDec(
node: ClassDeclaration,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog
): ApiDeclaration {
return {
id: getApiSectionId(anchorLink),
type: TypeKind.ClassKind,
label: node.getName() || 'Missing label',
description: getCommentsFromNode(node),
signature: getSignature(node, plugins, log),
children: node.getMembers().reduce((acc, m) => {
if (!isPrivate(m)) {
acc.push(
buildApiDeclaration(
m,
plugins,
log,
anchorLink.pluginName,
anchorLink.scope,
anchorLink.apiName
)
);
}
return acc;
}, [] as ApiDeclaration[]),
source: getSourceForNode(node),
};
}

View file

@ -0,0 +1,60 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import {
FunctionDeclaration,
MethodDeclaration,
ConstructorDeclaration,
Node,
MethodSignature,
} from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { buildApiDecsForParameters } from './build_parameter_decs';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { getCommentsFromNode } from './js_doc_utils';
import { getApiSectionId } from '../utils';
import { getJSDocReturnTagComment, getJSDocs, getJSDocTagNames } from './js_doc_utils';
import { getSourceForNode } from './utils';
import { getSignature } from './get_signature';
/**
* Takes the various function-like node declaration types and converts them into an ApiDeclaration.
* @param node
* @param plugins
* @param anchorLink
* @param log
*/
export function buildFunctionDec(
node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog
): ApiDeclaration {
const label = Node.isConstructorDeclaration(node)
? 'Constructor'
: node.getName() || '(WARN: Missing name)';
log.debug(`Getting function doc def for node ${label} of kind ${node.getKindName()}`);
return {
id: getApiSectionId(anchorLink),
type: TypeKind.FunctionKind,
label,
signature: getSignature(node, plugins, log),
description: getCommentsFromNode(node),
children: buildApiDecsForParameters(
node.getParameters(),
plugins,
anchorLink,
log,
getJSDocs(node)
),
tags: getJSDocTagNames(node),
returnComment: getJSDocReturnTagComment(node),
source: getSourceForNode(node),
};
}

View file

@ -0,0 +1,45 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { FunctionTypeNode, JSDoc } from 'ts-morph';
import { getApiSectionId } from '../utils';
import { getCommentsFromNode } from './js_doc_utils';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { buildApiDecsForParameters } from './build_parameter_decs';
import { extractImportReferences } from './extract_import_refs';
import { getJSDocReturnTagComment, getJSDocs, getJSDocTagNames } from './js_doc_utils';
import { getSourceForNode } from './utils';
export function buildApiDecFromFunctionType(
name: string,
node: FunctionTypeNode,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog,
jsDocs?: JSDoc[]
): ApiDeclaration {
log.debug(`Getting Function Type doc def for node ${name} of kind ${node.getKindName()}`);
return {
type: TypeKind.FunctionKind,
id: getApiSectionId(anchorLink),
label: name,
signature: extractImportReferences(node.getType().getText(), plugins, log),
description: getCommentsFromNode(node),
tags: jsDocs ? getJSDocTagNames(jsDocs) : [],
returnComment: jsDocs ? getJSDocReturnTagComment(jsDocs) : [],
children: buildApiDecsForParameters(
node.getParameters(),
plugins,
anchorLink,
log,
jsDocs || getJSDocs(node)
),
source: getSourceForNode(node),
};
}

View file

@ -0,0 +1,44 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { InterfaceDeclaration } from 'ts-morph';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { getCommentsFromNode } from './js_doc_utils';
import { buildApiDeclaration } from './build_api_declaration';
import { getSourceForNode } from './utils';
import { getApiSectionId } from '../utils';
import { getSignature } from './get_signature';
export function buildInterfaceDec(
node: InterfaceDeclaration,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog
): ApiDeclaration {
return {
id: getApiSectionId(anchorLink),
type: TypeKind.InterfaceKind,
label: node.getName(),
signature: getSignature(node, plugins, log),
description: getCommentsFromNode(node),
children: node
.getMembers()
.map((m) =>
buildApiDeclaration(
m,
plugins,
log,
anchorLink.pluginName,
anchorLink.scope,
anchorLink.apiName
)
),
source: getSourceForNode(node),
};
}

View file

@ -0,0 +1,65 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ParameterDeclaration, JSDoc, SyntaxKind } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { extractImportReferences } from './extract_import_refs';
import { AnchorLink, ApiDeclaration } from '../types';
import { buildApiDeclaration } from './build_api_declaration';
import { getJSDocParamComment } from './js_doc_utils';
import { getSourceForNode } from './utils';
import { getTypeKind } from './get_type_kind';
/**
* A helper function to capture function parameters, whether it comes from an arrow function, a regular function or
* a function type.
*
* @param params
* @param plugins
* @param anchorLink
* @param log
* @param jsDocs
*/
export function buildApiDecsForParameters(
params: ParameterDeclaration[],
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog,
jsDocs?: JSDoc[]
): ApiDeclaration[] {
return params.reduce((acc, param) => {
const label = param.getName();
log.debug(`Getting parameter doc def for ${label} of kind ${param.getKindName()}`);
// Literal types are non primitives that aren't references to other types. We add them as a more
// defined node, with children.
// If we don't want the docs to be too deeply nested we could avoid this special handling.
if (param.getTypeNode() && param.getTypeNode()!.getKind() === SyntaxKind.TypeLiteral) {
acc.push(
buildApiDeclaration(
param.getTypeNode()!,
plugins,
log,
anchorLink.pluginName,
anchorLink.scope,
anchorLink.apiName,
label
)
);
} else {
acc.push({
type: getTypeKind(param),
label,
isRequired: param.getType().isNullable() === false,
signature: extractImportReferences(param.getType().getText(), plugins, log),
description: jsDocs ? getJSDocParamComment(jsDocs, label) : [],
source: getSourceForNode(param),
});
}
return acc;
}, [] as ApiDeclaration[]);
}

View file

@ -0,0 +1,55 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { TypeLiteralNode } from 'ts-morph';
import { getApiSectionId } from '../utils';
import { getCommentsFromNode } from './js_doc_utils';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { buildApiDeclaration } from './build_api_declaration';
import { getSourceForNode } from './utils';
/**
* This captures function parameters that are object types, and makes sure their
* properties are recursively walked so they are expandable in the docs.
*
* The test verifying `crazyFunction` will fail without this special handling.
*
* @param node
* @param plugins
* @param anchorLink
* @param log
* @param name
*/
export function buildTypeLiteralDec(
node: TypeLiteralNode,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog,
name: string
): ApiDeclaration {
return {
id: getApiSectionId(anchorLink),
type: TypeKind.ObjectKind,
label: name,
description: getCommentsFromNode(node),
children: node
.getMembers()
.map((m) =>
buildApiDeclaration(
m,
plugins,
log,
anchorLink.pluginName,
anchorLink.scope,
anchorLink.apiName
)
),
source: getSourceForNode(node),
};
}

View file

@ -0,0 +1,82 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import {
VariableDeclaration,
Node,
PropertyAssignment,
PropertyDeclaration,
PropertySignature,
ShorthandPropertyAssignment,
} from 'ts-morph';
import { getApiSectionId } from '../utils';
import { getCommentsFromNode } from './js_doc_utils';
import { AnchorLink, ApiDeclaration, TypeKind } from '../types';
import { getArrowFunctionDec } from './build_arrow_fn_dec';
import { buildApiDeclaration } from './build_api_declaration';
import { getSourceForNode } from './utils';
import { getSignature } from './get_signature';
import { getTypeKind } from './get_type_kind';
/**
* Special handling for objects and arrow functions which are variable or property node types.
* Objects and arrow functions need their children extracted recursively. This uses the name from the
* node, but checks for an initializer to get inline arrow functions and objects defined recursively.
*
* @param node
* @param plugins
* @param anchorLink
* @param log
*/
export function buildVariableDec(
node:
| VariableDeclaration
| PropertyAssignment
| PropertyDeclaration
| PropertySignature
| ShorthandPropertyAssignment,
plugins: KibanaPlatformPlugin[],
anchorLink: AnchorLink,
log: ToolingLog
): ApiDeclaration {
log.debug('buildVariableDec for ' + node.getName());
const initializer = node.getInitializer();
// Recusively list object properties as children.
if (initializer && Node.isObjectLiteralExpression(initializer)) {
return {
id: getApiSectionId(anchorLink),
type: TypeKind.ObjectKind,
children: initializer.getProperties().map((prop) => {
return buildApiDeclaration(
prop,
plugins,
log,
anchorLink.pluginName,
anchorLink.scope,
anchorLink.apiName
);
}),
description: getCommentsFromNode(node),
label: node.getName(),
source: getSourceForNode(node),
};
} else if (initializer && Node.isArrowFunction(initializer)) {
return getArrowFunctionDec(node, initializer, plugins, anchorLink, log);
}
// Otherwise return it just as a single entry.
return {
id: getApiSectionId(anchorLink),
type: getTypeKind(node),
label: node.getName(),
description: getCommentsFromNode(node),
source: getSourceForNode(node),
signature: getSignature(node, plugins, log),
};
}

View file

@ -0,0 +1,96 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { getPluginApiDocId } from '../utils';
import { extractImportReferences } from './extract_import_refs';
import { ApiScope, Reference } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
const plugin = getKibanaPlatformPlugin('pluginA');
const plugins: KibanaPlatformPlugin[] = [plugin];
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
it('when there are no imports', () => {
const results = extractImportReferences(`(param: string) => Bar`, plugins, log);
expect(results.length).toBe(1);
expect(results[0]).toBe('(param: string) => Bar');
});
it('test extractImportReference', () => {
const results = extractImportReferences(
`(param: string) => import("${plugin.directory}/public/bar").Bar`,
plugins,
log
);
expect(results.length).toBe(2);
expect(results[0]).toBe('(param: string) => ');
expect(results[1]).toEqual({
text: 'Bar',
docId: getPluginApiDocId('plugin_a', log),
section: 'def-public.Bar',
pluginId: 'pluginA',
scope: ApiScope.CLIENT,
});
});
it('test extractImportReference with public folder nested under server folder', () => {
const results = extractImportReferences(
`import("${plugin.directory}/server/routes/public/bar").Bar`,
plugins,
log
);
expect(results.length).toBe(1);
expect(results[0]).toEqual({
text: 'Bar',
docId: getPluginApiDocId('plugin_a', log),
section: 'def-server.Bar',
pluginId: 'pluginA',
scope: ApiScope.SERVER,
});
});
it('test extractImportReference with two imports', () => {
const results = extractImportReferences(
`<I extends import("${plugin.directory}/public/foo/index").FooFoo, O extends import("${plugin.directory}/public/bar").Bar>`,
plugins,
log
);
expect(results.length).toBe(5);
expect(results[0]).toBe('<I extends ');
expect((results[1] as Reference).text).toBe('FooFoo');
expect(results[2]).toBe(', O extends ');
expect((results[3] as Reference).text).toBe('Bar');
expect(results[4]).toBe('>');
});
it('test extractImportReference with unknown imports', () => {
const results = extractImportReferences(
`<I extends import("/plugin_c/public/foo/index").FooFoo>`,
plugins,
log
);
expect(results.length).toBe(3);
expect(results[0]).toBe('<I extends ');
expect(results[1]).toBe('FooFoo');
expect(results[2]).toBe('>');
});
it('test single link', () => {
const results = extractImportReferences(
`import("${plugin.directory}/public/foo/index").FooFoo`,
plugins,
log
);
expect(results.length).toBe(1);
expect((results[0] as Reference).text).toBe('FooFoo');
});

View file

@ -0,0 +1,115 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { getApiSectionId, getPluginApiDocId, getPluginForPath } from '../utils';
import { ApiScope, TextWithLinks } from '../types';
/**
*
* @param text A string that may include an API item that was imported from another file. For example:
* "export type foo = string | import("kibana/src/plugins/a_plugin/public/path").Bar".
* @param plugins The list of registered Kibana plugins. Used to get the plugin id, which is then used to create
* the DocLink to that plugin's page, based off the relative path of any imports.
* @param log Logging utility for debuging
*
* @returns An array structure that can be used to create react DocLinks. For example, the above text would return
* something like:
* [ "export type foo = string | ", // Just a string for the pretext
* { id: "a_plugin", section: "public.Bar", text: "Bar" } // An object with info to create the DocLink.
* ]
*/
export function extractImportReferences(
text: string,
plugins: KibanaPlatformPlugin[],
log: ToolingLog
): TextWithLinks {
const texts: TextWithLinks = [];
let pos = 0;
let textSegment: string | undefined = text;
const max = 5;
while (textSegment) {
pos++;
if (pos > max) break;
const ref = extractImportRef(textSegment);
if (ref) {
const { name, path, index, length } = ref;
if (index !== 0) {
texts.push(textSegment.substr(0, index));
}
const plugin = getPluginForPath(path, plugins);
if (!plugin) {
if (path.indexOf('plugin') >= 0) {
log.warning('WARN: no plugin found for reference path ' + path);
}
// If we can't create a link for this, still remove the import("..."). part to make
// it easier to read.
const str = textSegment.substr(index + length - name.length, name.length);
if (str && str !== '') {
texts.push(str);
}
} else {
const section = getApiSectionId({
pluginName: plugin.manifest.id,
scope: getScopeFromPath(path, plugin, log),
apiName: name,
});
texts.push({
pluginId: plugin.manifest.id,
scope: getScopeFromPath(path, plugin, log),
docId: getPluginApiDocId(plugin.manifest.id, log, {
serviceFolders: plugin.manifest.serviceFolders,
apiPath: path,
directory: plugin.directory,
}),
section,
text: name,
});
}
textSegment = textSegment.substr(index + length);
} else {
if (textSegment && textSegment !== '') {
texts.push(textSegment);
}
textSegment = undefined;
}
}
return texts;
}
function extractImportRef(
str: string
): { path: string; name: string; index: number; length: number } | undefined {
const groups = str.match(/import\("(.*?)"\)\.(\w*)/);
if (groups) {
const path = groups[1];
const name = groups[2];
const index = groups.index!;
const length = groups[0].length;
return { path, name, index, length };
}
}
/**
*
* @param path An absolute path to a file inside a plugin directory.
*/
function getScopeFromPath(path: string, plugin: KibanaPlatformPlugin, log: ToolingLog): ApiScope {
if (path.startsWith(`${plugin.directory}/public/`)) {
return ApiScope.CLIENT;
} else if (path.startsWith(`${plugin.directory}/server/`)) {
return ApiScope.SERVER;
} else if (path.startsWith(`${plugin.directory}/common/`)) {
return ApiScope.COMMON;
} else {
log.warning(`Unexpected path encountered ${path}`);
return ApiScope.COMMON;
}
}

View file

@ -0,0 +1,120 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { Node, Type } from 'ts-morph';
import { isNamedNode } from '../tsmorph_utils';
import { Reference } from '../types';
import { extractImportReferences } from './extract_import_refs';
import { getTypeKind } from './get_type_kind';
/**
* Special logic for creating the signature based on the type of node. See https://github.com/dsherret/ts-morph/issues/923#issue-795332729
* for some issues that have been encountered in getting these accurate.
*
* By passing node to `getText`, ala `node.getType().getText(node)`, all reference links
* will be lost. However, if you do _not_ pass node, there are quite a few situations where it returns a reference
* to itself and has no helpful information.
*
* @param node
* @param plugins
* @param log
*/
export function getSignature(
node: Node,
plugins: KibanaPlatformPlugin[],
log: ToolingLog
): Array<string | Reference> | undefined {
let signature = '';
// node.getType() on a TypeAliasDeclaration is just a reference to itself. If we don't special case this, then
// `export type Foo = string | number;` would show up with a signagure of `Foo` that is a link to itself, instead of
// `string | number`.
if (Node.isTypeAliasDeclaration(node)) {
signature = getSignatureForTypeAlias(node.getType(), log, node);
} else if (Node.isFunctionDeclaration(node)) {
// See https://github.com/dsherret/ts-morph/issues/907#issue-770284331.
// Unfortunately this has to be manually pieced together, or it comes up as "typeof TheFunction"
const params = node
.getParameters()
.map((p) => `${p.getName()}: ${p.getType().getText()}`)
.join(', ');
const returnType = node.getReturnType().getText();
signature = `(${params}) => ${returnType}`;
} else if (Node.isInterfaceDeclaration(node) || Node.isClassDeclaration(node)) {
// Need to tack on manually any type parameters or "extends/implements" section.
const heritageClause = node
.getHeritageClauses()
.map((h) => {
const heritance = h.getText().indexOf('implements') > -1 ? 'implements' : 'extends';
return `${heritance} ${h.getTypeNodes().map((n) => n.getType().getText())}`;
})
.join(' ');
signature = `${node.getType().getText()}${heritageClause ? ' ' + heritageClause : ''}`;
} else {
// Here, 'node' is explicitly *not* passed in to `getText` otherwise arrow functions won't
// include reference links. Tests will break if you add it in here, or remove it from above.
// There is test coverage for all this oddness.
signature = node.getType().getText();
}
// Don't return the signature if it's the same as the type (string, string)
if (getTypeKind(node).toString() === signature) return undefined;
const referenceLinks = extractImportReferences(signature, plugins, log);
// Don't return the signature if it's a single self referential link.
if (
isNamedNode(node) &&
referenceLinks.length === 1 &&
typeof referenceLinks[0] === 'object' &&
(referenceLinks[0] as Reference).text === node.getName()
) {
return undefined;
}
return referenceLinks;
}
/**
* Not all types are handled here, but does return links for the more common ones.
*/
function getSignatureForTypeAlias(type: Type, log: ToolingLog, node?: Node): string {
if (type.isUnion()) {
return type
.getUnionTypes()
.map((nestedType) => getSignatureForTypeAlias(nestedType, log))
.join(' | ');
} else if (node && type.getCallSignatures().length >= 1) {
return type
.getCallSignatures()
.map((sig) => {
const params = sig
.getParameters()
.map((p) => `${p.getName()}: ${p.getTypeAtLocation(node).getText()}`)
.join(', ');
const returnType = sig.getReturnType().getText();
return `(${params}) => ${returnType}`;
})
.join(' ');
} else if (node) {
const symbol = node.getSymbol();
if (symbol) {
const declarations = symbol
.getDeclarations()
.map((d) => d.getType().getText(node))
.join(' ');
if (symbol.getDeclarations().length !== 1) {
log.error(
`Node is type alias declaration with more than one declaration. This is not handled! ${declarations} and node is ${node.getText()}`
);
}
return declarations;
}
}
return type.getText();
}

View file

@ -0,0 +1,69 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { Type, Node } from 'ts-morph';
import { TypeKind } from '../types';
export function getTypeKind(node: Node): TypeKind {
if (Node.isTypeAliasDeclaration(node)) {
return TypeKind.TypeKind;
} else {
return getTypeKindForType(node.getType());
}
}
function getTypeKindForType(type: Type): TypeKind {
// I think a string literal is also a string... but just in case, checking both.
if (type.isString() || type.isStringLiteral()) {
return TypeKind.StringKind;
} else if (type.isNumber() || type.isNumberLiteral()) {
return TypeKind.NumberKind;
// I could be wrong about this logic. Does this existance of a call signature mean it's a function?
} else if (type.getCallSignatures().length > 0) {
return TypeKind.FunctionKind;
} else if (type.isArray()) {
// Arrays are also objects, check this first.
return TypeKind.ArrayKind;
} else if (type.isObject()) {
return TypeKind.ObjectKind;
} else if (type.isBoolean() || type.isBooleanLiteral()) {
return TypeKind.BooleanKind;
} else if (type.isEnum() || type.isEnumLiteral()) {
return TypeKind.EnumKind;
} else if (type.isUnion()) {
// Special handling for "type | undefined" which happens alot and should be represented in docs as
// "type", but with an "optional" flag. Anything more complicated will just be returned as a
// "CompoundType".
if (getIsTypeOptional(type) && type.getUnionTypes().length === 2) {
const otherType = type.getUnionTypes().find((u) => u.isUndefined() === false);
if (otherType) {
return getTypeKindForType(otherType);
}
}
} else if (type.isAny()) {
return TypeKind.AnyKind;
} else if (type.isUnknown()) {
return TypeKind.UnknownKind;
}
if (type.isUnionOrIntersection()) {
return TypeKind.CompoundTypeKind;
}
return TypeKind.Uncategorized;
}
function getIsTypeOptional(type: Type): boolean {
if (type.isUnion()) {
const unions = type.getUnionTypes();
return unions.find((u) => u.isUndefined()) !== undefined;
} else {
return false;
}
}

View file

@ -0,0 +1,87 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { JSDoc, JSDocTag, Node } from 'ts-morph';
import { TextWithLinks } from '../types';
/**
* Extracts comments out of the node to use as the description.
*/
export function getCommentsFromNode(node: Node): TextWithLinks | undefined {
let comments: TextWithLinks | undefined;
const jsDocs = getJSDocs(node);
if (jsDocs) {
return getTextWithLinks(jsDocs.map((jsDoc) => jsDoc.getDescription()).join('\n'));
} else {
comments = getTextWithLinks(
node
.getLeadingCommentRanges()
.map((c) => c.getText())
.join('\n')
);
}
return comments;
}
export function getJSDocs(node: Node): JSDoc[] | undefined {
if (Node.isJSDocableNode(node)) {
return node.getJsDocs();
} else if (Node.isVariableDeclaration(node)) {
const gparent = node.getParent()?.getParent();
if (Node.isJSDocableNode(gparent)) {
return gparent.getJsDocs();
}
}
}
export function getJSDocReturnTagComment(node: Node | JSDoc[]): TextWithLinks {
const tags = getJSDocTags(node);
const returnTag = tags.find((tag) => Node.isJSDocReturnTag(tag));
if (returnTag) return getTextWithLinks(returnTag.getComment());
return [];
}
export function getJSDocParamComment(node: Node | JSDoc[], name: string): TextWithLinks {
const tags = getJSDocTags(node);
const paramTag = tags.find((tag) => Node.isJSDocParameterTag(tag) && tag.getName() === name);
if (paramTag) return getTextWithLinks(paramTag.getComment());
return [];
}
export function getJSDocTagNames(node: Node | JSDoc[]): string[] {
return getJSDocTags(node).reduce((tags, tag) => {
if (tag.getTagName() !== 'param' && tag.getTagName() !== 'returns') {
tags.push(tag.getTagName());
}
return tags;
}, [] as string[]);
}
function getJSDocTags(node: Node | JSDoc[]): JSDocTag[] {
const jsDocs = node instanceof Array ? node : getJSDocs(node);
if (!jsDocs) return [];
return jsDocs.reduce((tagsAcc, jsDoc) => {
tagsAcc.push(...jsDoc.getTags());
return tagsAcc;
}, [] as JSDocTag[]);
}
/**
* TODO. This feature is not implemented yet. It will be used to create links for comments
* that use {@link AnotherAPIItemInThisPlugin}.
*
* @param text
*/
function getTextWithLinks(text?: string): TextWithLinks {
if (text) return [text];
else return [];
// TODO:
// Replace `@links` in comments with relative api links.
}

View file

@ -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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { REPO_ROOT, kibanaPackageJson } from '@kbn/utils';
import { ParameterDeclaration, ClassMemberTypes, Node } from 'ts-morph';
import { SourceLink } from '../types';
export function isPrivate(node: ParameterDeclaration | ClassMemberTypes): boolean {
return node.getModifiers().find((mod) => mod.getText() === 'private') !== undefined;
}
/**
* Change the absolute path into a relative one.
*/
function getRelativePath(fullPath: string): string {
return Path.relative(REPO_ROOT, fullPath);
}
export function getSourceForNode(node: Node): SourceLink {
const path = getRelativePath(node.getSourceFile().getFilePath());
const lineNumber = node.getStartLineNumber();
return {
path,
lineNumber,
link: `https://github.com/elastic/kibana/tree/${kibanaPackageJson.branch}${path}#L${lineNumber}`,
};
}

View file

@ -0,0 +1,152 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import { REPO_ROOT, run } from '@kbn/dev-utils';
import { Project } from 'ts-morph';
import { getPluginApi } from './get_plugin_api';
import { writePluginDocs } from './mdx/write_plugin_mdx_docs';
import { ApiDeclaration, PluginApi } from './types';
import { findPlugins } from './find_plugins';
import { removeBrokenLinks } from './utils';
export interface PluginInfo {
apiCount: number;
apiCountMissingComments: number;
id: string;
missingApiItems: string[];
}
export function runBuildApiDocsCli() {
run(
async ({ log }) => {
const project = getTsProject(REPO_ROOT);
const plugins = findPlugins();
const pluginInfos: {
[key: string]: PluginInfo;
} = {};
const outputFolder = Path.resolve(REPO_ROOT, 'api_docs');
if (!Fs.existsSync(outputFolder)) {
Fs.mkdirSync(outputFolder);
} else {
// Delete all files except the README that warns about the auto-generated nature of
// the folder.
const files = Fs.readdirSync(outputFolder);
files.forEach((file) => {
if (file.indexOf('README.md') < 0) {
Fs.rmSync(Path.resolve(outputFolder, file));
}
});
}
const pluginApiMap: { [key: string]: PluginApi } = {};
plugins.map((plugin) => {
pluginApiMap[plugin.manifest.id] = getPluginApi(project, plugin, plugins, log);
});
const missingApiItems: { [key: string]: string[] } = {};
plugins.forEach((plugin) => {
const id = plugin.manifest.id;
const pluginApi = pluginApiMap[id];
removeBrokenLinks(pluginApi, missingApiItems, pluginApiMap);
});
plugins.forEach((plugin) => {
const id = plugin.manifest.id;
const pluginApi = pluginApiMap[id];
const info = {
id,
apiCount: countApiForPlugin(pluginApi),
apiCountMissingComments: countMissingCommentsApiForPlugin(pluginApi),
missingApiItems: missingApiItems[id],
};
if (info.apiCount > 0) {
writePluginDocs(outputFolder, pluginApi, log);
pluginInfos[id] = info;
}
});
// eslint-disable-next-line no-console
console.table(pluginInfos);
},
{
log: {
defaultLevel: 'debug',
},
}
);
}
function getTsProject(repoPath: string) {
const xpackTsConfig = `${repoPath}/tsconfig.json`;
const project = new Project({
tsConfigFilePath: xpackTsConfig,
});
project.addSourceFilesAtPaths(`${repoPath}/x-pack/plugins/**/*{.d.ts,.ts}`);
project.resolveSourceFileDependencies();
return project;
}
function countMissingCommentsApiForPlugin(doc: PluginApi) {
return (
doc.client.reduce((sum, def) => {
return sum + countMissingCommentsForApi(def);
}, 0) +
doc.server.reduce((sum, def) => {
return sum + countMissingCommentsForApi(def);
}, 0) +
doc.common.reduce((sum, def) => {
return sum + countMissingCommentsForApi(def);
}, 0)
);
}
function countMissingCommentsForApi(doc: ApiDeclaration): number {
const missingCnt = doc.description && doc.description.length > 0 ? 0 : 1;
if (!doc.children) return missingCnt;
else
return (
missingCnt +
doc.children.reduce((sum, child) => {
return sum + countMissingCommentsForApi(child);
}, 0)
);
}
function countApiForPlugin(doc: PluginApi) {
return (
doc.client.reduce((sum, def) => {
return sum + countApi(def);
}, 0) +
doc.server.reduce((sum, def) => {
return sum + countApi(def);
}, 0) +
doc.common.reduce((sum, def) => {
return sum + countApi(def);
}, 0)
);
}
function countApi(doc: ApiDeclaration): number {
if (!doc.children) return 1;
else
return (
1 +
doc.children.reduce((sum, child) => {
return sum + countApi(child);
}, 0)
);
}

View file

@ -0,0 +1,25 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { getPluginSearchPaths } from '@kbn/config';
import { simpleKibanaPlatformPluginDiscovery, REPO_ROOT } from '@kbn/dev-utils';
export function findPlugins() {
const pluginSearchPaths = getPluginSearchPaths({
rootDir: REPO_ROOT,
oss: false,
examples: false,
});
return simpleKibanaPlatformPluginDiscovery(pluginSearchPaths, [
// discover "core" as a plugin
Path.resolve(REPO_ROOT, 'src/core'),
]);
}

View file

@ -0,0 +1,73 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { Project, SourceFile, Node } from 'ts-morph';
import { ApiScope } from './types';
import { isNamedNode, getSourceFileMatching } from './tsmorph_utils';
/**
* Determines which file in the project to grab nodes from, depending on the plugin and scope, then returns those nodes.
*
* @param project - TS project.
* @param plugin - The plugin we are interested in.
* @param scope - The "scope" of the API we want to extract: public, server or common.
* @param log - logging utility.
*
* @return Every publically exported Node from the given plugin and scope (public, server, common).
*/
export function getDeclarationNodesForPluginScope(
project: Project,
plugin: KibanaPlatformPlugin,
scope: ApiScope,
log: ToolingLog
): Node[] {
const path = Path.join(`${plugin.directory}`, scope.toString(), 'index.ts');
const file = getSourceFileMatching(project, path);
if (file) {
return getExportedFileDeclarations(file, log);
} else {
log.debug(`No file found: ${path}`);
return [];
}
}
/**
*
* @param source the file we want to extract exported declaration nodes from.
* @param log
*/
function getExportedFileDeclarations(source: SourceFile, log: ToolingLog): Node[] {
const nodes: Node[] = [];
const exported = source.getExportedDeclarations();
// Filter out the exported declarations that exist only for the plugin system itself.
exported.forEach((val) => {
val.forEach((ed) => {
const name: string = isNamedNode(ed) ? ed.getName() : '';
// Every plugin will have an export called "plugin". Don't bother listing
// it, it's only for the plugin infrastructure.
// Config is also a common export on the server side that is just for the
// plugin infrastructure.
if (name === 'plugin' || name === 'config') {
return;
}
if (name && name !== '') {
nodes.push(ed);
} else {
log.warning(`API with missing name encountered.`);
}
});
});
log.debug(`Collected ${nodes.length} exports from file ${source.getFilePath()}`);
return nodes;
}

View file

@ -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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { Node, Project, Type } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { ApiScope, Lifecycle } from './types';
import { ApiDeclaration, PluginApi } from './types';
import { buildApiDeclaration } from './build_api_declarations/build_api_declaration';
import { getDeclarationNodesForPluginScope } from './get_declaration_nodes_for_plugin';
import { getSourceFileMatching } from './tsmorph_utils';
/**
* Collects all the information neccessary to generate this plugins mdx api file(s).
*/
export function getPluginApi(
project: Project,
plugin: KibanaPlatformPlugin,
plugins: KibanaPlatformPlugin[],
log: ToolingLog
): PluginApi {
const client = getDeclarations(project, plugin, ApiScope.CLIENT, plugins, log);
const server = getDeclarations(project, plugin, ApiScope.SERVER, plugins, log);
const common = getDeclarations(project, plugin, ApiScope.COMMON, plugins, log);
return {
id: plugin.manifest.id,
client,
server,
common,
serviceFolders: plugin.manifest.serviceFolders,
};
}
/**
*
* @returns All exported ApiDeclarations for the given plugin and scope (client, server, common), broken into
* groups of typescript kinds (functions, classes, interfaces, etc).
*/
function getDeclarations(
project: Project,
plugin: KibanaPlatformPlugin,
scope: ApiScope,
plugins: KibanaPlatformPlugin[],
log: ToolingLog
): ApiDeclaration[] {
const nodes = getDeclarationNodesForPluginScope(project, plugin, scope, log);
const contractTypes = getContractTypes(project, plugin, scope);
const declarations = nodes.reduce<ApiDeclaration[]>((acc, node) => {
const apiDec = buildApiDeclaration(node, plugins, log, plugin.manifest.id, scope);
// Filter out apis with the @internal flag on them.
if (!apiDec.tags || apiDec.tags.indexOf('internal') < 0) {
// buildApiDeclaration doesn't set the lifecycle, so we set it here.
const lifecycle = getLifecycle(node, contractTypes);
acc.push({
...apiDec,
lifecycle,
initialIsOpen: lifecycle !== undefined,
});
}
return acc;
}, []);
// We have all the ApiDeclarations, now lets group them by typescript kinds.
return declarations;
}
/**
* Checks if this node is one of the special start or setup contract interface types. We pull these
* to the top of the API docs.
*
* @param node ts-morph node
* @param contractTypeNames the start and setup contract interface names
* @returns Which, if any, lifecycle contract this node happens to represent.
*/
function getLifecycle(
node: Node,
contractTypeNames: { start?: Type; setup?: Type }
): Lifecycle | undefined {
// Note this logic is not tested if a plugin uses "as",
// like export { Setup as MyPluginSetup } from ..."
if (contractTypeNames.start && node.getType() === contractTypeNames.start) {
return Lifecycle.START;
}
if (contractTypeNames.setup && node.getType() === contractTypeNames.setup) {
return Lifecycle.SETUP;
}
}
/**
*
* @param project
* @param plugin the plugin we are interested in.
* @param scope Whether we are interested in the client or server plugin contracts.
* Common scope will never return anything.
* @returns the name of the two types used for Start and Setup contracts, if they
* exist and were exported from the plugin class.
*/
function getContractTypes(
project: Project,
plugin: KibanaPlatformPlugin,
scope: ApiScope
): { setup?: Type; start?: Type } {
const contractTypes: { setup?: Type; start?: Type } = {};
const file = getSourceFileMatching(
project,
Path.join(`${plugin.directory}`, scope.toString(), 'plugin.ts')
);
if (file) {
file.getClasses().forEach((c) => {
c.getImplements().forEach((i) => {
let index = 0;
i.getType()
.getTypeArguments()
.forEach((arg) => {
// Setup type comes first
if (index === 0) {
contractTypes.setup = arg;
} else if (index === 1) {
contractTypes.start = arg;
}
index++;
});
});
});
}
return contractTypes;
}

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export * from './build_api_docs_cli';

View file

@ -0,0 +1,51 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { Project } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { PluginApi } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
import { getPluginApi } from '../get_plugin_api';
import { splitApisByFolder } from './write_plugin_split_by_folder';
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
let doc: PluginApi;
beforeAll(() => {
const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json');
const project = new Project({
tsConfigFilePath,
});
expect(project.getSourceFiles().length).toBeGreaterThan(0);
const pluginA = getKibanaPlatformPlugin('pluginA');
pluginA.manifest.serviceFolders = ['foo'];
const plugins: KibanaPlatformPlugin[] = [pluginA];
doc = getPluginApi(project, plugins[0], plugins, log);
});
test('foo service has all exports', () => {
expect(doc?.client.length).toBe(33);
const split = splitApisByFolder(doc);
expect(split.length).toBe(2);
const fooDoc = split.find((d) => d.id === 'pluginA.foo');
const mainDoc = split.find((d) => d.id === 'pluginA');
expect(fooDoc?.common.length).toBe(1);
expect(fooDoc?.client.length).toBe(2);
expect(mainDoc?.client.length).toBe(31);
});

View file

@ -0,0 +1,143 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import fs from 'fs';
import Path from 'path';
import dedent from 'dedent';
import { PluginApi, ScopeApi } from '../types';
import {
countScopeApi,
getPluginApiDocId,
snakeToCamel,
camelToSnake,
groupPluginApi,
} from '../utils';
import { writePluginDocSplitByFolder } from './write_plugin_split_by_folder';
/**
* Converts the plugin doc to mdx and writes it into the file system. If the plugin,
* has serviceFolders specified in it's kibana.json, multiple mdx files will be written.
*
* @param folder The location the mdx files will be written too.
* @param doc Contains the information of the plugin that will be written into mdx.
* @param log Used for logging debug and error information.
*/
export function writePluginDocs(folder: string, doc: PluginApi, log: ToolingLog): void {
if (doc.serviceFolders) {
log.debug(`Splitting plugin ${doc.id}`);
writePluginDocSplitByFolder(folder, doc, log);
} else {
writePluginDoc(folder, doc, log);
}
}
function hasPublicApi(doc: PluginApi): boolean {
return doc.client.length > 0 || doc.server.length > 0 || doc.common.length > 0;
}
/**
* Converts the plugin doc to mdx and writes it into the file system. Ignores
* the serviceFolders setting. Use {@link writePluginDocs} if you wish to split
* the plugin into potentially multiple mdx files.
*
* @param folder The location the mdx file will be written too.
* @param doc Contains the information of the plugin that will be written into mdx.
* @param log Used for logging debug and error information.
*/
export function writePluginDoc(folder: string, doc: PluginApi, log: ToolingLog): void {
if (!hasPublicApi(doc)) {
log.debug(`${doc.id} does not have a public api. Skipping.`);
return;
}
log.debug(`Writing plugin file for ${doc.id}`);
const fileName = getFileName(doc.id);
// Append "obj" to avoid special names in here. 'case' is one in particular that
// caused issues.
const json = getJsonName(fileName) + 'Obj';
let mdx =
dedent(`
---
id: ${getPluginApiDocId(doc.id, log)}
slug: /kibana-dev-docs/${doc.id}PluginApi
title: ${doc.id}
image: https://source.unsplash.com/400x175/?github
summary: API docs for the ${doc.id} plugin
date: 2020-11-16
tags: ['contributor', 'dev', 'apidocs', 'kibana', '${doc.id}']
---
import ${json} from './${fileName}.json';
`) + '\n\n';
const scopedDoc = {
...doc,
client: groupPluginApi(doc.client),
common: groupPluginApi(doc.common),
server: groupPluginApi(doc.server),
};
fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc));
mdx += scopApiToMdx(scopedDoc.client, 'Client', json, 'client');
mdx += scopApiToMdx(scopedDoc.server, 'Server', json, 'server');
mdx += scopApiToMdx(scopedDoc.common, 'Common', json, 'common');
fs.writeFileSync(Path.resolve(folder, fileName + '.mdx'), mdx);
}
function getJsonName(name: string): string {
return snakeToCamel(getFileName(name));
}
function getFileName(name: string): string {
return camelToSnake(name.replace('.', '_'));
}
function scopApiToMdx(scope: ScopeApi, title: string, json: string, scopeName: string): string {
let mdx = '';
if (countScopeApi(scope) > 0) {
mdx += `## ${title}\n\n`;
if (scope.setup) {
mdx += `### Setup\n`;
mdx += `<DocDefinitionList data={[${json}.${scopeName}.setup]}/>\n`;
}
if (scope.start) {
mdx += `### Start\n`;
mdx += `<DocDefinitionList data={[${json}.${scopeName}.start]}/>\n`;
}
if (scope.objects.length > 0) {
mdx += `### Objects\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.objects}/>\n`;
}
if (scope.functions.length > 0) {
mdx += `### Functions\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.functions}/>\n`;
}
if (scope.classes.length > 0) {
mdx += `### Classes\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.classes}/>\n`;
}
if (scope.interfaces.length > 0) {
mdx += `### Interfaces\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.interfaces}/>\n`;
}
if (scope.enums.length > 0) {
mdx += `### Enums\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.enums}/>\n`;
}
if (scope.misc.length > 0) {
mdx += `### Consts, variables and types\n`;
mdx += `<DocDefinitionList data={${json}.${scopeName}.misc}/>\n`;
}
}
return mdx;
}

View file

@ -0,0 +1,72 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { Project } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { splitApisByFolder } from './write_plugin_split_by_folder';
import { getPluginApi } from '../get_plugin_api';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
it('splitApisByFolder test splitting plugin by service folder', () => {
const project = new Project({ useInMemoryFileSystem: true });
project.createSourceFile(
'src/plugins/example/public/index.ts',
`
import { bar } from './a_service/foo/bar';
import { Zed, zed } from './a_service/zed';
import { util } from './utils';
export { bar, Zed, zed, mainFoo, util };
`
);
project.createSourceFile(
'src/plugins/example/public/a_service/zed.ts',
`export const zed: string = 'hi';
export interface Zed = { zed: string }`
);
project.createSourceFile(
'src/plugins/example/public/a_service/foo/bar.ts',
`export const bar: string = 'bar';`
);
project.createSourceFile(
'src/plugins/example/public/utils.ts',
`export const util: string = 'Util';`
);
const plugin = getKibanaPlatformPlugin('example', '/src/plugins/example');
const plugins: KibanaPlatformPlugin[] = [
{
...plugin,
manifest: {
...plugin.manifest,
serviceFolders: ['a_service'],
},
},
];
const doc = getPluginApi(project, plugins[0], plugins, log);
const docs = splitApisByFolder(doc);
// The api at the main level, and one on a service level.
expect(docs.length).toBe(2);
const mainDoc = docs.find((d) => d.id === 'example');
expect(mainDoc).toBeDefined();
const serviceDoc = docs.find((d) => d.id === 'example.aService');
expect(serviceDoc).toBeDefined();
expect(serviceDoc?.client.length).toBe(3);
});

View file

@ -0,0 +1,69 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { snakeToCamel } from '../utils';
import { PluginApi, ApiDeclaration } from '../types';
import { writePluginDoc } from './write_plugin_mdx_docs';
export function writePluginDocSplitByFolder(folder: string, doc: PluginApi, log: ToolingLog) {
const apisByFolder = splitApisByFolder(doc);
log.debug(`Split ${doc.id} into ${apisByFolder.length} services`);
apisByFolder.forEach((docDef) => {
writePluginDoc(folder, docDef, log);
});
}
export function splitApisByFolder(pluginDoc: PluginApi): PluginApi[] {
const pluginDocDefsByFolder: { [key: string]: PluginApi } = {};
const mainPluginDocDef = createServicePluginDocDef(pluginDoc);
pluginDoc.client.forEach((dec: ApiDeclaration) => {
addSection(dec, 'client', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!);
});
pluginDoc.server.forEach((dec: ApiDeclaration) => {
addSection(dec, 'server', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!);
});
pluginDoc.common.forEach((dec: ApiDeclaration) => {
addSection(dec, 'common', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!);
});
return [...Object.values(pluginDocDefsByFolder), mainPluginDocDef];
}
function addSection(
dec: ApiDeclaration,
scope: 'client' | 'server' | 'common',
mainPluginDocDef: PluginApi,
pluginServices: { [key: string]: PluginApi },
serviceFolders: readonly string[]
) {
const scopeFolder = scope === 'client' ? 'public' : scope;
const matchGroup = dec.source.path.match(`.*?\/${scopeFolder}\/([^\/]*?)\/`);
const serviceFolderName = matchGroup ? matchGroup[1] : undefined;
if (serviceFolderName && serviceFolders.find((f) => f === serviceFolderName)) {
const service = snakeToCamel(serviceFolderName);
if (!pluginServices[service]) {
pluginServices[service] = createServicePluginDocDef(mainPluginDocDef, service);
}
pluginServices[service][scope].push(dec);
} else {
mainPluginDocDef[scope].push(dec);
}
}
function createServicePluginDocDef(pluginDoc: PluginApi, service?: string): PluginApi {
return {
id: service ? pluginDoc.id + '.' + service : pluginDoc.id,
client: [],
server: [],
common: [],
};
}

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export const commonFoo = 'COMMON VAR!';

View file

@ -0,0 +1,13 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { commonFoo } from './foo';
export interface ImACommonType {
goo: number;
}

View file

@ -0,0 +1,7 @@
{
"id": "pluginA",
"summary": "This an example plugin for testing the api documentation system",
"version": "kibana",
"serviceFolders": ["foo"]
}

View file

@ -0,0 +1,80 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
/* eslint-disable max-classes-per-file */
import { ImAType } from './types';
/**
* An interface with a generic.
*/
export interface WithGen<T = number> {
t: T;
}
export interface AnotherInterface<T> {
t: T;
}
export class ExampleClass<T> implements AnotherInterface<T> {
/**
* This should not be exposed in the docs!
*/
private privateVar: string;
public component?: React.ComponentType;
constructor(public t: T) {
this.privateVar = 'hi';
}
/**
* an arrow fn on a class.
* @param a im a string
*/
arrowFn = (a: ImAType): ImAType => a;
/**
* A function on a class.
* @param a a param
*/
getVar(a: ImAType) {
return this.privateVar;
}
}
export class CrazyClass<P extends ImAType = any> extends ExampleClass<WithGen<P>> {}
/**
* This is an example interface so we can see how it appears inside the API
* documentation system.
*/
export interface ExampleInterface extends AnotherInterface<string> {
/**
* This gets a promise that resolves to a string.
*/
getAPromiseThatResolvesToString: () => Promise<string>;
/**
* This function takes a generic. It was sometimes being tripped on
* and returned as an unknown type with no signature.
*/
aFnWithGen: <T>(t: T) => void;
/**
* These are not coming back properly.
*/
aFn(): void;
}
/**
* An interface that has a react component.
*/
export interface IReturnAReactComponent {
component: React.ComponentType;
}

View file

@ -0,0 +1,76 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { CrazyClass } from './classes';
import { notAnArrowFn } from './fns';
import { ImAType } from './types';
/**
* Some of the plugins wrap static exports in an object to create
* a namespace like this.
*/
export const aPretendNamespaceObj = {
/**
* The docs should show this inline comment.
*/
notAnArrowFn,
/**
* Should this comment show up?
*/
aPropertyMisdirection: notAnArrowFn,
/**
* I'm a property inline fun.
*/
aPropertyInlineFn: (a: ImAType): ImAType => {
return a;
},
/**
* The only way for this to have a comment is to grab this.
*/
aPropertyStr: 'Hi',
/**
* Will this nested object have it's children extracted appropriately?
*/
nestedObj: {
foo: 'string',
},
};
/**
* This is a complicated union type
*/
export const aUnionProperty: string | number | (() => string) | CrazyClass = '6';
/**
* This is an array of strings. The type is explicit.
*/
export const aStrArray: string[] = ['hi', 'bye'];
/**
* This is an array of numbers. The type is implied.
*/
export const aNumArray = [1, 3, 4];
/**
* A string that says hi to you!
*/
export const aStr: string = 'hi';
/**
* It's a number. A special number.
*/
export const aNum = 10;
/**
* I'm a type of string, but more specifically, a literal string type.
*/
export const literalString = 'HI';

View file

@ -0,0 +1,78 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { TypeWithGeneric, ImAType } from './types';
/**
* This is a non arrow function.
*
* @param a The letter A
* @param b Feed me to the function
* @param c So many params
* @param d a great param
* @param e Another comment
* @returns something!
*/
export function notAnArrowFn(
a: string,
b: number | undefined,
c: TypeWithGeneric<string>,
d: ImAType,
e?: string
): TypeWithGeneric<string> {
return ['hi'];
}
/**
* This is an arrow function.
*
* @param a The letter A
* @param b Feed me to the function
* @param c So many params
* @param d a great param
* @param e Another comment
* @returns something!
*/
export const arrowFn = (
a: string,
b: number | undefined,
c: TypeWithGeneric<string>,
d: ImAType,
e?: string
): TypeWithGeneric<string> => {
return ['hi'];
};
/**
* Who would write such a complicated function?? Ewwww.
*
* According to https://jsdoc.app/tags-param.html#parameters-with-properties,
* this is how destructured arguements should be commented.
*
* @param obj A very crazy parameter that is destructured when passing in.
* @param objWithFn Im an object with a function. Destructed!
* @param objWithFn.fn A fn.
* @param objWithStr Im an object with a string. Destructed!
* @param objWithStr.str A str.
*
* @returns I have no idea.
*
*/
export const crazyFunction = (
obj: { hi: string },
{ fn }: { fn: (foo: { param: string }) => number },
{ str }: { str: string }
) => () => () => fn({ param: str });
interface ImNotExported {
foo: string;
}
export const fnWithNonExportedRef = (a: ImNotExported) => 'shi';
export type NotAnArrowFnType = typeof notAnArrowFn;

View file

@ -0,0 +1,13 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export const doTheFooFnThing = () => {};
export type FooType = () => 'foo';
export type ImNotExportedFromIndex = () => { bar: string };

View file

@ -0,0 +1,24 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { PluginA, Setup, Start, SearchSpec } from './plugin';
export { Setup, Start, SearchSpec };
export { doTheFooFnThing, FooType } from './foo';
export * from './fns';
export * from './classes';
export * from './const_vars';
export * from './types';
export const imAnAny: any = 'hi';
export const imAnUnknown: unknown = 'hi';
export function plugin() {
return new PluginA();
}

View file

@ -0,0 +1,172 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
// The logic for grabbing Setup and Start types relies on implementing an
// interface with at least two type args. Since the test code isn't adding
// every import file, use this mock, otherwise it won't have the type and will
// fail.
interface PluginMock<Sp, St> {
setup(): Sp;
start(): St;
}
/**
* The SearchSpec interface contains settings for creating a new SearchService, like
* username and password.
*/
export interface SearchSpec {
/**
* Stores the username. Duh,
*/
username: string;
/**
* Stores the password. I hope it's encrypted!
*/
password: string;
}
/**
* The type of search language.
*/
export enum SearchLanguage {
/**
* The SQL SearchLanguage type
*/
SQL,
/**
* The EQL SearchLanguage type. Support sequences.
*/
EQL,
/**
* The ES DSL SearchLanguage type. It's the default.
*/
ES_DSL,
}
/**
* Access start functionality from your plugin's start function by adding the example
* plugin as a dependency.
*
* ```ts
* Class MyPlugin {
* start(core: CoreDependencies, { example }: PluginDependencies) {
* // Here you can access this functionality.
* example.getSearchLanguage();
* }
* }
* ```
*/
export interface Start {
/**
* @returns The currently selected {@link SearchLanguage}
*/
getSearchLanguage: () => SearchLanguage;
}
/**
* Access setup functionality from your plugin's setup function by adding the example
* plugin as a dependency.
*
* ```ts
* Class MyPlugin {
* setup(core: CoreDependencies, { example }: PluginDependencies) {
* // Here you can access this functionality.
* example.getSearchService();
* }
* }
* ```
*/
export interface Setup {
/**
* A factory function that returns a new instance of Foo based
* on the spec. We aren't sure if this is a good function so it's marked
* beta. That should be clear in the docs because of the js doc tag.
*
* @param searchSpec Provide the settings neccessary to create a new Search Service
*
* @returns the id of the search service.
*
* @beta
*/
getSearchService: (searchSpec: SearchSpec) => string;
/**
* This uses an inlined object type rather than referencing an exported type, which is discouraged.
* prefer the way {@link getSearchService} is typed.
*
* @param searchSpec Provide the settings neccessary to create a new Search Service
*/
getSearchService2: (searchSpec: { username: string; password: string }) => string;
/**
* This function does the thing and it's so good at it! But we decided to deprecate it
* anyway. I hope that's clear to developers in the docs!
*
* @param thingOne Thing one comment
* @param thingTwo ThingTwo comment
* @param thingThree Thing three is an object with a nested var
*
* @deprecated
*
*/
doTheThing: (thingOne: number, thingTwo: string, thingThree: { nestedVar: number }) => void;
/**
* Who would write such a complicated function?? Ew, how will the obj parameter appear in docs?
*
* @param obj A funky parameter.
*
* @returns It's hard to tell but I think this returns a function that returns an object with a
* property that is a function that returns a string. Whoa.
*
*/
fnWithInlineParams: (obj: {
fn: (foo: { param: string }) => number;
}) => () => { retFoo: () => string };
/**
* Hi, I'm a comment for an id string!
*/
id: string;
}
/**
* This comment won't show up in the API docs.
*/
function getSearchService() {
return 'hi';
}
function fnWithInlineParams() {
return () => ({
retFoo: () => 'hi',
});
}
/**
* The example search plugin is a fake plugin that is built only to test our api documentation system.
*
*/
export class PluginA implements PluginMock<Setup, Start> {
setup() {
return {
// Don't put comments here - they won't show up. What's here shouldn't matter because
// the API documentation system works off the type `Setup`.
doTheThing: () => {},
fnWithInlineParams,
getSearchService,
getSearchService2: getSearchService,
registerSearch: () => {},
id: '123',
};
}
start() {
return { getSearchLanguage: () => SearchLanguage.EQL };
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ImACommonType } from '../common';
import { FooType, ImNotExportedFromIndex } from './foo';
/**
* How should a potentially undefined type show up.
*/
export type StringOrUndefinedType = string | undefined;
export type TypeWithGeneric<T> = T[];
export type ImAType = string | number | TypeWithGeneric<string> | FooType | ImACommonType;
/**
* This is a type that defines a function.
*
* @param t This is a generic T type. It can be anything.
*/
export type FnWithGeneric = <T>(t: T) => TypeWithGeneric<T>;
/**
* Comments on enums.
*/
export enum DayOfWeek {
THURSDAY,
FRIDAY, // How about this comment, hmmm?
SATURDAY,
}
/**
* Calling node.getSymbol().getDeclarations() will return > 1 declaration.
*/
export type MultipleDeclarationsType = TypeWithGeneric<typeof DayOfWeek>;
export type IRefANotExportedType = ImNotExportedFromIndex | { zed: 'hi' };
export interface ImAnObject {
foo: FnWithGeneric;
}

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"incremental": false,
"strictNullChecks": true,
},
"include": ["./**/*"]
}

View file

@ -0,0 +1,397 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import fs from 'fs';
import Path from 'path';
import { Project } from 'ts-morph';
import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils';
import { writePluginDocs } from '../mdx/write_plugin_mdx_docs';
import { ApiDeclaration, PluginApi, Reference, TextWithLinks, TypeKind } from '../types';
import { getKibanaPlatformPlugin } from './kibana_platform_plugin_mock';
import { getPluginApi } from '../get_plugin_api';
import { groupPluginApi } from '../utils';
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
let doc: PluginApi;
let mdxOutputFolder: string;
function linkCount(signature: TextWithLinks): number {
return signature.reduce((cnt, next) => (typeof next === 'string' ? cnt : cnt + 1), 0);
}
function fnIsCorrect(fn: ApiDeclaration | undefined) {
expect(fn).toBeDefined();
expect(fn?.type).toBe(TypeKind.FunctionKind);
// The signature should contain a link to ExampleInterface param.
expect(fn?.signature).toBeDefined();
expect(linkCount(fn!.signature!)).toBe(3);
expect(fn?.children!.length).toBe(5);
expect(fn?.returnComment!.length).toBe(1);
const p1 = fn?.children!.find((c) => c.label === 'a');
expect(p1).toBeDefined();
expect(p1!.type).toBe(TypeKind.StringKind);
expect(p1!.isRequired).toBe(true);
expect(p1!.signature?.length).toBe(1);
expect(linkCount(p1!.signature!)).toBe(0);
const p2 = fn?.children!.find((c) => c.label === 'b');
expect(p2).toBeDefined();
expect(p2!.isRequired).toBe(false);
expect(p2!.type).toBe(TypeKind.NumberKind);
expect(p2!.signature?.length).toBe(1);
expect(linkCount(p2!.signature!)).toBe(0);
const p3 = fn?.children!.find((c) => c.label === 'c');
expect(p3).toBeDefined();
expect(p3!.isRequired).toBe(true);
expect(p3!.type).toBe(TypeKind.ArrayKind);
expect(linkCount(p3!.signature!)).toBe(1);
const p4 = fn?.children!.find((c) => c.label === 'd');
expect(p4).toBeDefined();
expect(p4!.isRequired).toBe(true);
expect(p4!.type).toBe(TypeKind.CompoundTypeKind);
expect(p4!.signature?.length).toBe(1);
expect(linkCount(p4!.signature!)).toBe(1);
const p5 = fn?.children!.find((c) => c.label === 'e');
expect(p5).toBeDefined();
expect(p5!.isRequired).toBe(false);
expect(p5!.type).toBe(TypeKind.StringKind);
expect(p5!.signature?.length).toBe(1);
expect(linkCount(p5!.signature!)).toBe(0);
}
beforeAll(() => {
const tsConfigFilePath = Path.resolve(__dirname, '__fixtures__/src/tsconfig.json');
const project = new Project({
tsConfigFilePath,
});
expect(project.getSourceFiles().length).toBeGreaterThan(0);
const pluginA = getKibanaPlatformPlugin('pluginA');
pluginA.manifest.serviceFolders = ['foo'];
const plugins: KibanaPlatformPlugin[] = [pluginA];
doc = getPluginApi(project, plugins[0], plugins, log);
mdxOutputFolder = Path.resolve(__dirname, 'snapshots');
writePluginDocs(mdxOutputFolder, doc, log);
});
it('Setup type is extracted', () => {
const grouped = groupPluginApi(doc.client);
expect(grouped.setup).toBeDefined();
});
it('service mdx file was created', () => {
expect(fs.existsSync(Path.resolve(mdxOutputFolder, 'plugin_a_foo.mdx'))).toBe(true);
});
it('Setup type has comment', () => {
const grouped = groupPluginApi(doc.client);
expect(grouped.setup!.description).toBeDefined();
expect(grouped.setup!.description).toMatchInlineSnapshot(`
Array [
"
Access setup functionality from your plugin's setup function by adding the example
plugin as a dependency.
\`\`\`ts
Class MyPlugin {
setup(core: CoreDependencies, { example }: PluginDependencies) {
// Here you can access this functionality.
example.getSearchService();
}
}
\`\`\`",
]
`);
});
it('const exported from common folder is correct', () => {
const fooConst = doc.common.find((c) => c.label === 'commonFoo');
expect(fooConst).toBeDefined();
expect(fooConst!.source.path.replace(Path.sep, '/')).toContain(
'src/plugin_a/common/foo/index.ts'
);
expect(fooConst!.signature![0]).toBe('"COMMON VAR!"');
});
describe('functions', () => {
it('function referencing missing type has link removed', () => {
const fn = doc.client.find((c) => c.label === 'fnWithNonExportedRef');
expect(linkCount(fn?.signature!)).toBe(0);
});
it('arrow function is exported correctly', () => {
const fn = doc.client.find((c) => c.label === 'arrowFn');
// Using the same data as the not an arrow function so this is refactored.
fnIsCorrect(fn);
});
it('non arrow function is exported correctly', () => {
const fn = doc.client.find((c) => c.label === 'notAnArrowFn');
// Using the same data as the arrow function so this is refactored.
fnIsCorrect(fn);
});
it('crazyFunction is typed correctly', () => {
const fn = doc.client!.find((c) => c.label === 'crazyFunction');
expect(fn).toBeDefined();
const obj = fn?.children?.find((c) => c.label === 'obj');
expect(obj).toBeDefined();
expect(obj!.children?.length).toBe(1);
const hi = obj?.children?.find((c) => c.label === 'hi');
expect(hi).toBeDefined();
const obj2 = fn?.children?.find((c) => c.label === '{ fn }');
expect(obj2).toBeDefined();
expect(obj2!.children?.length).toBe(1);
const fn2 = obj2?.children?.find((c) => c.label === 'fn');
expect(fn2).toBeDefined();
expect(fn2?.type).toBe(TypeKind.FunctionKind);
});
});
describe('objects', () => {
it('Object exported correctly', () => {
const obj = doc.client.find((c) => c.label === 'aPretendNamespaceObj');
expect(obj).toBeDefined();
const fn = obj?.children?.find((c) => c.label === 'notAnArrowFn');
expect(fn?.signature).toBeDefined();
// Should just be typeof notAnArrowFn.
expect(linkCount(fn?.signature!)).toBe(1);
// Comment should be the inline one.
expect(fn?.description).toMatchInlineSnapshot(`
Array [
"/**
* The docs should show this inline comment.
*/",
]
`);
const fn2 = obj?.children?.find((c) => c.label === 'aPropertyInlineFn');
expect(fn2?.signature).toBeDefined();
// Should include 2 links to ImAType
expect(linkCount(fn2?.signature!)).toBe(2);
expect(fn2?.children).toBeDefined();
const nestedObj = obj?.children?.find((c) => c.label === 'nestedObj');
// We aren't giving objects a signature. The children should contain all the information.
expect(nestedObj?.signature).toBeUndefined();
expect(nestedObj?.children).toBeDefined();
expect(nestedObj?.type).toBe(TypeKind.ObjectKind);
const foo = nestedObj?.children?.find((c) => c.label === 'foo');
expect(foo?.type).toBe(TypeKind.StringKind);
});
});
describe('Misc types', () => {
it('Explicitly typed array is returned with the correct type', () => {
const aStrArray = doc.client.find((c) => c.label === 'aStrArray');
expect(aStrArray).toBeDefined();
expect(aStrArray?.type).toBe(TypeKind.ArrayKind);
});
it('Implicitly typed array is returned with the correct type', () => {
const aNumArray = doc.client.find((c) => c.label === 'aNumArray');
expect(aNumArray).toBeDefined();
expect(aNumArray?.type).toBe(TypeKind.ArrayKind);
});
it('Explicitly typed string is returned with the correct type', () => {
const aStr = doc.client.find((c) => c.label === 'aStr');
expect(aStr).toBeDefined();
expect(aStr?.type).toBe(TypeKind.StringKind);
// signature would be the same as type, so it should be removed.
expect(aStr?.signature).toBeUndefined();
});
it('Implicitly typed number is returned with the correct type', () => {
const aNum = doc.client.find((c) => c.label === 'aNum');
expect(aNum).toBeDefined();
expect(aNum?.type).toBe(TypeKind.NumberKind);
});
it('aUnionProperty is exported as a CompoundType with a call signature', () => {
const prop = doc.client.find((c) => c.label === 'aUnionProperty');
expect(prop).toBeDefined();
expect(prop?.type).toBe(TypeKind.CompoundTypeKind);
expect(linkCount(prop?.signature!)).toBe(1);
});
it('Function type is exported correctly', () => {
const fnType = doc.client.find((c) => c.label === 'FnWithGeneric');
expect(fnType).toBeDefined();
expect(fnType?.type).toBe(TypeKind.TypeKind);
expect(fnType?.signature!).toMatchInlineSnapshot(`
Array [
"(t: T) => ",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.TypeWithGeneric",
"text": "TypeWithGeneric",
},
"<T>",
]
`);
expect(linkCount(fnType?.signature!)).toBe(1);
});
it('Union type is exported correctly', () => {
const type = doc.client.find((c) => c.label === 'ImAType');
expect(type).toBeDefined();
expect(type?.type).toBe(TypeKind.TypeKind);
expect(type?.signature).toBeDefined();
expect(type?.signature!).toMatchInlineSnapshot(`
Array [
"string | number | ",
Object {
"docId": "kibPluginAFooPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.FooType",
"text": "FooType",
},
" | ",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.TypeWithGeneric",
"text": "TypeWithGeneric",
},
"<string> | ",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "common",
"section": "def-common.ImACommonType",
"text": "ImACommonType",
},
]
`);
expect(linkCount(type?.signature!)).toBe(3);
expect((type!.signature![1] as Reference).docId).toBe('kibPluginAFooPluginApi');
});
});
describe('interfaces and classes', () => {
it('Basic interface exported correctly', () => {
const anInterface = doc.client.find((c) => c.label === 'IReturnAReactComponent');
expect(anInterface).toBeDefined();
// Make sure it doesn't include a self referential link.
expect(anInterface?.signature).toBeUndefined();
});
it('Interface which extends exported correctly', () => {
const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface');
expect(exampleInterface).toBeDefined();
expect(exampleInterface?.signature).toBeDefined();
expect(exampleInterface?.type).toBe(TypeKind.InterfaceKind);
expect(linkCount(exampleInterface?.signature!)).toBe(2);
// TODO: uncomment if the bug is fixed.
// This is wrong, the link should be to `AnotherInterface`
// Another bug, this link is not being captured.
expect(exampleInterface?.signature).toMatchInlineSnapshot(`
Array [
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.ExampleInterface",
"text": "ExampleInterface",
},
" extends ",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.AnotherInterface",
"text": "AnotherInterface",
},
"<string>",
]
`);
});
it('Non arrow function on interface is exported as function type', () => {
const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface');
expect(exampleInterface).toBeDefined();
const fn = exampleInterface!.children?.find((c) => c.label === 'aFn');
expect(fn).toBeDefined();
expect(fn?.type).toBe(TypeKind.FunctionKind);
});
it('Class exported correctly', () => {
const clss = doc.client.find((c) => c.label === 'CrazyClass');
expect(clss).toBeDefined();
expect(clss?.signature).toBeDefined();
expect(clss?.type).toBe(TypeKind.ClassKind);
expect(clss?.signature).toMatchInlineSnapshot(`
Array [
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.CrazyClass",
"text": "CrazyClass",
},
"<P> extends ",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.ExampleClass",
"text": "ExampleClass",
},
"<",
Object {
"docId": "kibPluginAPluginApi",
"pluginId": "pluginA",
"scope": "public",
"section": "def-public.WithGen",
"text": "WithGen",
},
"<P>>",
]
`);
expect(linkCount(clss?.signature!)).toBe(3);
});
it('Function with generic inside interface is exported with function type', () => {
const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface');
expect(exampleInterface).toBeDefined();
const fnWithGeneric = exampleInterface?.children?.find((c) => c.label === 'aFnWithGen');
expect(fnWithGeneric).toBeDefined();
expect(fnWithGeneric?.type).toBe(TypeKind.FunctionKind);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin } from '@kbn/dev-utils';
import Path from 'path';
export function getKibanaPlatformPlugin(id: string, dir?: string): KibanaPlatformPlugin {
const directory = dir ?? Path.resolve(__dirname, '__fixtures__/src/plugin_a');
return {
manifest: {
id,
ui: true,
server: true,
kibanaVersion: '1',
version: '1',
serviceFolders: [],
requiredPlugins: [],
requiredBundles: [],
optionalPlugins: [],
extraPublicDirs: [],
},
directory,
manifestPath: Path.resolve(directory, 'kibana.json'),
};
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,34 @@
---
id: kibPluginAPluginApi
slug: /kibana-dev-docs/pluginAPluginApi
title: pluginA
image: https://source.unsplash.com/400x175/?github
summary: API docs for the pluginA plugin
date: 2020-11-16
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA']
---
import pluginAObj from './plugin_a.json';
## Client
### Setup
<DocDefinitionList data={[pluginAObj.client.setup]}/>
### Start
<DocDefinitionList data={[pluginAObj.client.start]}/>
### Objects
<DocDefinitionList data={pluginAObj.client.objects}/>
### Functions
<DocDefinitionList data={pluginAObj.client.functions}/>
### Classes
<DocDefinitionList data={pluginAObj.client.classes}/>
### Interfaces
<DocDefinitionList data={pluginAObj.client.interfaces}/>
### Enums
<DocDefinitionList data={pluginAObj.client.enums}/>
### Consts, variables and types
<DocDefinitionList data={pluginAObj.client.misc}/>
## Common
### Interfaces
<DocDefinitionList data={pluginAObj.common.interfaces}/>

View file

@ -0,0 +1 @@
{"id":"pluginA.foo","client":{"classes":[],"functions":[{"id":"def-public.doTheFooFnThing","type":"Function","children":[],"signature":["() => void"],"description":[],"label":"doTheFooFnThing","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9"},"returnComment":[],"initialIsOpen":false}],"interfaces":[],"enums":[],"misc":[{"id":"def-public.FooType","type":"Type","label":"FooType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11"},"signature":["() => \"foo\""],"initialIsOpen":false}],"objects":[]},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[{"id":"def-common.commonFoo","type":"string","label":"commonFoo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9"},"signature":["\"COMMON VAR!\""],"initialIsOpen":false}],"objects":[]}}

View file

@ -0,0 +1,22 @@
---
id: kibPluginAFooPluginApi
slug: /kibana-dev-docs/pluginA.fooPluginApi
title: pluginA.foo
image: https://source.unsplash.com/400x175/?github
summary: API docs for the pluginA.foo plugin
date: 2020-11-16
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo']
---
import pluginAFooObj from './plugin_a_foo.json';
## Client
### Functions
<DocDefinitionList data={pluginAFooObj.client.functions}/>
### Consts, variables and types
<DocDefinitionList data={pluginAFooObj.client.misc}/>
## Common
### Consts, variables and types
<DocDefinitionList data={pluginAFooObj.common.misc}/>

View file

@ -0,0 +1,38 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { Node, SourceFile, Project } from 'ts-morph';
export interface NamedNode extends Node {
getName(): string;
}
/**
* ts-morph has a Node.isNamedNode fn but it isn't returning true for all types
* that will have node.getName.
*/
export function isNamedNode(node: Node | NamedNode): node is NamedNode {
return (node as NamedNode).getName !== undefined;
}
/**
* Helper function to find a source file at a given location. Used to extract
* index.ts files at a given scope.
*
* @param project The ts morph project which contains all the source files
* @param absolutePath The absolute path of the file we want to find
* @returns a source file that exists at the location of the relative path.
*/
export function getSourceFileMatching(
project: Project,
absolutePath: string
): SourceFile | undefined {
return project.getSourceFiles().find((file) => {
return file.getFilePath().startsWith(absolutePath);
});
}

View file

@ -0,0 +1,200 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export interface AnchorLink {
/**
* The plugin that contains the API being referenced.
*/
pluginName: string;
/**
* It's possible the client and the server both emit an API with
* the same name so we need scope in here to add uniqueness.
*/
scope: ApiScope;
/**
* The name of the api.
*/
apiName: string;
}
/**
* The kinds of typescript types we want to show in the docs. `Unknown` is used if
* we aren't accounting for a particular type. See {@link getPropertyTypeKind}
*/
export enum TypeKind {
ClassKind = 'Class',
FunctionKind = 'Function',
ObjectKind = 'Object',
EnumKind = 'Enum',
InterfaceKind = 'Interface',
/**
* Maps to the typescript syntax kind `TypeReferences`. For example,
* export type FooFn = () => string will be a TypeKind, not a FunctionKind.
*/
TypeKind = 'Type',
/**
* Uncategorized is used if a type is encountered that isn't handled.
*/
Uncategorized = 'Uncategorized',
UnknownKind = 'Unknown', // Maps to the unknown typescript type
AnyKind = 'Any', // Maps to the any typescript type
StringKind = 'string',
NumberKind = 'number',
BooleanKind = 'boolean',
ArrayKind = 'Array',
/**
* This will cover things like string | number, or A & B, for lack of something better to put here.
*/
CompoundTypeKind = 'CompoundType',
}
export interface ScopeApi {
setup?: ApiDeclaration;
start?: ApiDeclaration;
functions: ApiDeclaration[];
objects: ApiDeclaration[];
classes: ApiDeclaration[];
interfaces: ApiDeclaration[];
enums: ApiDeclaration[];
misc: ApiDeclaration[];
}
export interface PluginApi {
id: string;
serviceFolders?: readonly string[];
client: ApiDeclaration[];
server: ApiDeclaration[];
common: ApiDeclaration[];
}
/**
* This is used for displaying code or comments that may contain reference links. For example, a function
* signature that is `(a: import("src/plugin_b").Bar) => void` will be parsed into the following Array:
*
* ```ts
* [
* '(a: ',
* { docId: 'pluginB', section: 'Bar', text: 'Bar' },
* ') => void'
* ]
* ```
*
* This is then used to render text with nested DocLinks so it looks like this:
*
* `(a: => <DocLink docId="pluginB" section="Bar" text="Bar"/>) => void`
*/
export type TextWithLinks = Array<string | Reference>;
/**
* The information neccessary to build a DocLink.
*/
export interface Reference {
pluginId: string;
scope: ApiScope;
docId: string;
section: string;
text: string;
}
/**
* This type should eventually be replaced by something inside elastic-docs.
* It's what will be passed to an elastic-docs supplied component to make
* the API docs pretty.
*/
export interface ApiDeclaration {
/**
* Used for an anchor link to this Api. Can't use label as there can be two labels with the same
* text within the Client section and the Server section.
*/
id?: string;
/**
* The name of the api.
*/
label: string;
/**
* Should the list be expanded or collapsed initially?
*/
initialIsOpen?: boolean;
/**
* The kind of type this API represents, e.g. string, number, Object, Interface, Class.
*/
type: TypeKind;
/**
* Certain types have children. For instance classes have class members, functions will list
* their parameters here, classes will list their class members here, and objects and interfaces
* will list their properties.
*/
children?: ApiDeclaration[];
/**
* TODO
*/
isRequired?: boolean;
/**
* Api node comment.
*/
description?: TextWithLinks;
/**
* If the type is a function, it's signature should be displayed. Currently this overlaps with type
* sometimes, and will sometimes be left empty for large types (like classes and interfaces).
*/
signature?: TextWithLinks;
/**
* Relevant for functions with @returns comments.
*/
returnComment?: TextWithLinks;
/**
* Will contain the tags on a comment, like `beta` or `deprecated`.
* Won't include param or returns tags.
*/
tags?: string[];
/**
* Every plugn that exposes functionality from their setup and start contract
* should have a single exported type for each. These get pulled to the top because
* they are accessed differently than other exported functionality and types.
*/
lifecycle?: Lifecycle;
/**
* Used to create links to github to view the code for this API.
*/
source: SourceLink;
}
export interface SourceLink {
path: string;
lineNumber: number;
link: string;
}
/**
* Developers will need to know whether these APIs are available on the client, server, or both.
*/
export enum ApiScope {
CLIENT = 'public',
SERVER = 'server',
COMMON = 'common',
}
/**
* Start and Setup interfaces are special - their functionality is not imported statically but
* accessible via the dependent plugins start and setup functions.
*/
export enum Lifecycle {
START = 'start',
SETUP = 'setup',
}

View file

@ -0,0 +1,83 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import Path from 'path';
import { Project } from 'ts-morph';
import { findPlugins } from './find_plugins';
import { getPluginApi } from './get_plugin_api';
import { getKibanaPlatformPlugin } from './tests/kibana_platform_plugin_mock';
import { PluginApi } from './types';
import { getPluginForPath, getServiceForPath, removeBrokenLinks } from './utils';
const log = new ToolingLog({
level: 'debug',
writeTo: process.stdout,
});
it('test getPluginForPath', () => {
const plugins = findPlugins();
const path = Path.resolve(__dirname, '../../../../src/plugins/embeddable/public/service/file.ts');
expect(getPluginForPath(path, plugins)).toBeDefined();
});
it('test getServiceForPath', () => {
expect(getServiceForPath('src/plugins/embed/public/service/file.ts', 'src/plugins/embed')).toBe(
'service'
);
expect(
getServiceForPath('src/plugins/embed/public/service/subfolder/file.ts', 'src/plugins/embed')
).toBe('service');
expect(
getServiceForPath('src/plugins/embed/public/file.ts', 'src/plugins/embed')
).toBeUndefined();
expect(
getServiceForPath('/src/plugins/embed/server/another_service/index', '/src/plugins/embed')
).toBe('another_service');
expect(getServiceForPath('src/plugins/embed/server/no_ending', 'src/plugins/embed')).toBe(
undefined
);
expect(
getServiceForPath('src/plugins/embed/server/routes/public/foo/index.ts', 'src/plugins/embed')
).toBe('routes');
expect(getServiceForPath('src/plugins/embed/server/f.ts', 'src/plugins/embed')).toBeUndefined();
expect(
getServiceForPath(
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index',
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a'
)
).toBe('foo');
});
it('test removeBrokenLinks', () => {
const tsConfigFilePath = Path.resolve(__dirname, 'tests/__fixtures__/src/tsconfig.json');
const project = new Project({
tsConfigFilePath,
});
expect(project.getSourceFiles().length).toBeGreaterThan(0);
const pluginA = getKibanaPlatformPlugin('pluginA');
pluginA.manifest.serviceFolders = ['foo'];
const plugins: KibanaPlatformPlugin[] = [pluginA];
const pluginApiMap: { [key: string]: PluginApi } = {};
plugins.map((plugin) => {
pluginApiMap[plugin.manifest.id] = getPluginApi(project, plugin, plugins, log);
});
const missingApiItems: { [key: string]: string[] } = {};
plugins.forEach((plugin) => {
const id = plugin.manifest.id;
const pluginApi = pluginApiMap[id];
removeBrokenLinks(pluginApi, missingApiItems, pluginApiMap);
});
expect(missingApiItems.pluginA.indexOf('public.ImNotExportedFromIndex')).toBeGreaterThan(-1);
});

View file

@ -0,0 +1,208 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import {
AnchorLink,
ApiDeclaration,
ScopeApi,
TypeKind,
Lifecycle,
PluginApi,
ApiScope,
} from './types';
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const camelToSnake = (str: string): string => str.replace(/([A-Z])/g, '_$1').toLowerCase();
export const snakeToCamel = (str: string): string =>
str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', ''));
/**
* Returns the plugin that the file belongs to.
* @param path An absolute file path that can point to a file nested inside a plugin
* @param plugins A list of plugins to search through.
*/
export function getPluginForPath(
path: string,
plugins: KibanaPlatformPlugin[]
): KibanaPlatformPlugin | undefined {
return plugins.find((plugin) => path.startsWith(plugin.directory));
}
/**
* Groups ApiDeclarations by typescript kind - classes, functions, enums, etc, so they
* can be displayed separately in the mdx files.
*/
export function groupPluginApi(declarations: ApiDeclaration[]): ScopeApi {
const scope = createEmptyScope();
declarations.forEach((declaration) => {
addApiDeclarationToScope(declaration, scope);
});
return scope;
}
function escapeRegExp(regexp: string) {
return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
* If the file is at the top level, returns undefined, otherwise returns the
* name of the first nested folder in the plugin. For example a path of
* 'src/plugins/data/public/search_services/file.ts' would return 'search_service' while
* 'src/plugin/data/server/file.ts' would return undefined.
* @param path
*/
export function getServiceForPath(path: string, pluginDirectory: string): string | undefined {
const dir = escapeRegExp(pluginDirectory);
const publicMatchGroups = path.match(`${dir}\/public\/([^\/]*)\/`);
const serverMatchGroups = path.match(`${dir}\/server\/([^\/]*)\/`);
const commonMatchGroups = path.match(`${dir}\/common\/([^\/]*)\/`);
if (publicMatchGroups && publicMatchGroups.length > 1) {
return publicMatchGroups[1];
} else if (serverMatchGroups && serverMatchGroups.length > 1) {
return serverMatchGroups[1];
} else if (commonMatchGroups && commonMatchGroups.length > 1) {
return commonMatchGroups[1];
}
}
export function getPluginApiDocId(
id: string,
log: ToolingLog,
serviceInfo?: {
serviceFolders: readonly string[];
apiPath: string;
directory: string;
}
) {
let service = '';
const cleanName = id.replace('.', '_');
if (serviceInfo) {
const serviceName = getServiceForPath(serviceInfo.apiPath, serviceInfo.directory);
log.debug(
`Service for path ${serviceInfo.apiPath} and ${serviceInfo.directory} is ${serviceName}`
);
const serviceFolder = serviceInfo.serviceFolders?.find((f) => f === serviceName);
if (serviceFolder) {
service = snakeToCamel(serviceFolder);
}
}
return `kib${capitalize(snakeToCamel(cleanName)) + capitalize(service)}PluginApi`;
}
export function getApiSectionId(link: AnchorLink) {
const id = `def-${link.scope}.${link.apiName}`.replace(' ', '-');
return id;
}
export function countScopeApi(api: ScopeApi): number {
return (
(api.setup ? 1 : 0) +
(api.start ? 1 : 0) +
api.classes.length +
api.interfaces.length +
api.functions.length +
api.objects.length +
api.enums.length +
api.misc.length
);
}
export function createEmptyScope(): ScopeApi {
return {
classes: [],
functions: [],
interfaces: [],
enums: [],
misc: [],
objects: [],
};
}
/**
* Takes the ApiDeclaration and puts it in the appropriate section of the ScopeApi based
* on its TypeKind.
*/
export function addApiDeclarationToScope(declaration: ApiDeclaration, scope: ScopeApi): void {
if (declaration.lifecycle === Lifecycle.SETUP) {
scope.setup = declaration;
} else if (declaration.lifecycle === Lifecycle.START) {
scope.start = declaration;
} else {
switch (declaration.type) {
case TypeKind.ClassKind:
scope.classes.push(declaration);
break;
case TypeKind.InterfaceKind:
scope.interfaces.push(declaration);
break;
case TypeKind.EnumKind:
scope.enums.push(declaration);
break;
case TypeKind.FunctionKind:
scope.functions.push(declaration);
break;
case TypeKind.ObjectKind:
scope.objects.push(declaration);
break;
default:
scope.misc.push(declaration);
}
}
}
export function removeBrokenLinks(
pluginApi: PluginApi,
missingApiItems: { [key: string]: string[] },
pluginApiMap: { [key: string]: PluginApi }
) {
(['client', 'common', 'server'] as Array<'client' | 'server' | 'common'>).forEach((scope) => {
pluginApi[scope].forEach((api) => {
if (api.signature) {
api.signature = api.signature.map((sig) => {
if (typeof sig !== 'string') {
if (apiItemExists(sig.text, sig.scope, pluginApiMap[sig.pluginId]) === false) {
if (missingApiItems[sig.pluginId] === undefined) {
missingApiItems[sig.pluginId] = [];
}
missingApiItems[sig.pluginId].push(`${sig.scope}.${sig.text}`);
return sig.text;
}
}
return sig;
});
}
});
});
}
function apiItemExists(name: string, scope: ApiScope, pluginApi: PluginApi): boolean {
return (
pluginApi[scopeAccessor(scope)].findIndex((dec: ApiDeclaration) => dec.label === name) >= 0
);
}
function scopeAccessor(scope: ApiScope): 'server' | 'common' | 'client' {
switch (scope) {
case ApiScope.CLIENT:
return 'client';
case ApiScope.SERVER:
return 'server';
default:
return 'common';
}
}

View file

@ -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 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 or the Server
* Side Public License, v 1.
*/
export * from './release_notes';
export * from './api_docs';

View file

@ -8,5 +8,8 @@
},
"include": [
"src/**/*"
],
"exclude": [
"**/__fixtures__/**/*"
]
}

File diff suppressed because it is too large Load diff

10
scripts/build_api_docs.js Normal file
View file

@ -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 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 or the Server
* Side Public License, v 1.
*/
require('../src/setup_node_env');
require('@kbn/docs-utils').runBuildApiDocsCli();

View file

@ -7,4 +7,4 @@
*/
require('../src/setup_node_env/no_transpilation');
require('@kbn/release-notes').runReleaseNotesCli();
require('@kbn/docs-utils').runReleaseNotesCli();

7
src/core/kibana.json Normal file
View file

@ -0,0 +1,7 @@
{
"id": "core",
"summary": "The core plugin has core functionality",
"version": "kibana",
"serviceFolders": ["http", "saved_objects", "chrome", "application"]
}

View file

@ -47,6 +47,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
server: true,
extraPublicDirs: true,
requiredBundles: true,
serviceFolders: true,
};
return new Set(Object.keys(manifestFields));

View file

@ -169,6 +169,12 @@ export interface PluginManifest {
* @deprecated
*/
readonly extraPublicDirs?: string[];
/**
* Only used for the automatically generated API documentation. Specifying service
* folders will cause your plugin API reference to be broken up into sub sections.
*/
readonly serviceFolders?: readonly string[];
}
/**

View file

@ -1883,6 +1883,7 @@ export interface PluginManifest {
readonly requiredBundles: readonly string[];
readonly requiredPlugins: readonly PluginName[];
readonly server: boolean;
readonly serviceFolders?: readonly string[];
readonly ui: boolean;
readonly version: string;
}
@ -3198,9 +3199,9 @@ export const validBodyOutput: readonly ["data", "stream"];
// Warnings were encountered during analysis:
//
// src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
```

View file

@ -10,6 +10,7 @@
"share",
"inspector"
],
"serviceFolders": ["search", "index_patterns", "query", "autocomplete", "ui", "field_formats"],
"optionalPlugins": ["usageCollection"],
"extraPublicDirs": ["common"],
"requiredBundles": [

View file

@ -2107,6 +2107,14 @@
enabled "2.0.x"
kuler "^2.0.0"
"@dsherret/to-absolute-glob@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c"
integrity sha1-H2R13IvZdM6gei2vOGSzF7HdMyw=
dependencies:
is-absolute "^1.0.0"
is-negated-glob "^1.0.0"
"@elastic/apm-rum-core@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.7.0.tgz#2213987285324781e2ebeca607f3a71245da5a84"
@ -3428,6 +3436,10 @@
version "0.0.0"
uid ""
"@kbn/docs-utils@link:packages/kbn-docs-utils":
version "0.0.0"
uid ""
"@kbn/es-archiver@link:packages/kbn-es-archiver":
version "0.0.0"
uid ""
@ -3484,10 +3496,6 @@
version "0.0.0"
uid ""
"@kbn/release-notes@link:packages/kbn-release-notes":
version "0.0.0"
uid ""
"@kbn/std@link:packages/kbn-std":
version "0.0.0"
uid ""
@ -5134,6 +5142,18 @@
dependencies:
"@babel/runtime" "^7.10.2"
"@ts-morph/common@~0.7.0":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.7.3.tgz#380020c278e4aa6cecedf362a1157591d1003267"
integrity sha512-M6Tcu0EZDLL8Ht7WAYz7yJfDZ9eArhqR8XZ9Mk3q8jwU6MKFAttrw3JtW4JhneqTz7pZMv4XaimEdXI0E4K4rg==
dependencies:
"@dsherret/to-absolute-glob" "^2.0.2"
fast-glob "^3.2.4"
is-negated-glob "^1.0.0"
mkdirp "^1.0.4"
multimatch "^5.0.0"
typescript "~4.1.2"
"@turf/along@6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@turf/along/-/along-6.0.1.tgz#595cecdc48fc7fcfa83c940a8e3eb24d4c2e04d4"
@ -10561,6 +10581,11 @@ coa@^2.0.2:
chalk "^2.4.1"
q "^1.1.2"
code-block-writer@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@ -21176,6 +21201,17 @@ multimatch@^4.0.0:
arrify "^2.0.1"
minimatch "^3.0.4"
multimatch@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6"
integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==
dependencies:
"@types/minimatch" "^3.0.3"
array-differ "^3.0.0"
array-union "^2.1.0"
arrify "^2.0.1"
minimatch "^3.0.4"
multiparty@^4.1.2:
version "4.2.1"
resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13"
@ -28444,6 +28480,15 @@ ts-log@2.1.4:
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.1.4.tgz#063c5ad1cbab5d49d258d18015963489fb6fb59a"
integrity sha512-P1EJSoyV+N3bR/IWFeAqXzKPZwHpnLY6j7j58mAvewHRipo+BQM2Y1f9Y9BjEQznKwgqqZm7H8iuixmssU7tYQ==
ts-morph@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-9.1.0.tgz#10d2088387c71f3c674f82492a3cec1e3538f0dd"
integrity sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q==
dependencies:
"@dsherret/to-absolute-glob" "^2.0.2"
"@ts-morph/common" "~0.7.0"
code-block-writer "^10.1.1"
ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
@ -28648,7 +28693,7 @@ typescript-tuple@^2.2.1:
dependencies:
typescript-compare "^0.0.2"
typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2:
typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2, typescript@~4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==