Skip to content

Commit

Permalink
auto generated mdx api doc system
Browse files Browse the repository at this point in the history
  • Loading branch information
stacey-gammon committed Dec 22, 2020
1 parent 2a4c65e commit 5653c59
Show file tree
Hide file tree
Showing 66 changed files with 1,953 additions and 2,537 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.aws-config.json
.signing-config.json
/api_docs
.ackrc
/.es
/.chromium
Expand Down
27 changes: 0 additions & 27 deletions docs/development/example_plugin.mdx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

import Path from 'path';
import loadJsonFile from 'load-json-file';
import { REPO_ROOT } from '@kbn/utils';

export interface KibanaPlatformPlugin {
readonly directory: string;
readonly manifestPath: string;
readonly manifest: Manifest;
readonly relativeDirectory: string;
}

function isValidDepsDeclaration(input: unknown, type: string): string[] {
Expand All @@ -40,6 +42,7 @@ interface Manifest {
server: boolean;
kibanaVersion: string;
version: string;
serviceFolders: readonly string[];
requiredPlugins: readonly string[];
optionalPlugins: readonly string[];
requiredBundles: readonly string[];
Expand All @@ -65,6 +68,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP
}

return {
relativeDirectory: Path.dirname(manifestPath).slice(REPO_ROOT.length),
directory: Path.dirname(manifestPath),
manifestPath,
manifest: {
Expand All @@ -75,6 +79,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'),
Expand Down
2 changes: 1 addition & 1 deletion scripts/build_api_docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
*/

require('../src/setup_node_env');
require('../src/dev/build_api_docs/build_from_ts_morph/run');
require('../src/dev/build_api_docs/run');
7 changes: 7 additions & 0 deletions src/core/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "core",
"summary": "The core plugin has core functionality",
"version": "kibana",
"serviceFolders": ["http", "saved_objects", "chrome", "application"]
}

67 changes: 67 additions & 0 deletions src/dev/build_api_docs/api/get_api_nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Path from 'path';
import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { Project, SourceFile, Node } from 'ts-morph';
import { ApiScope } from './types';
import { isNamedNode } from '../tsmorph_utils';
import { getSourceFileMatching } from './utils';

export function getApisForPluginScope(
project: Project,
plugin: KibanaPlatformPlugin,
scope: ApiScope,
log: ToolingLog
): Node[] {
const path = Path.join(`${plugin.relativeDirectory}`, scope.toString(), 'index.ts');
const file = getSourceFileMatching(project, path);

if (file) {
return getApisForFile(file, log);
} else {
return [];
}
}

function getApisForFile(source: SourceFile, log: ToolingLog): Node[] {
const nodes: Node[] = [];
const exported = source.getExportedDeclarations();

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@
* under the License.
*/

import { DocDef } from '../types';
/**
* 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',
}

export function getMdxForDocDef(doc: DocDef) {
return `<DocDefinitionList data={[${JSON.stringify(doc)}]} />`;
/**
* 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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { Project, SourceFile } from 'ts-morph';
import { ApiScope } from './types';

import { escapeText, getApiSectionId } from '../mdx_utils';
import { ApiDocTypes } from '../types';
export function getScopeFromPath(path: string, log: ToolingLog): ApiScope {
if (path.indexOf('/public/') >= 0) {
return ApiScope.CLIENT;
} else if (path.indexOf('/server/') >= 0) {
return ApiScope.SERVER;
} else if (path.indexOf('/common/') >= 0) {
return ApiScope.COMMON;
} else {
log.warning(`Unexpected path encountered ${path}`);
return ApiScope.COMMON;
}
}

export function getName(doc: ApiDocTypes) {
return doc.anchorLink && doc.name
? `<a name="${getApiSectionId(
doc.anchorLink.pluginName,
doc.anchorLink.publicOrServer,
doc.name
)}"></a>\n## ${escapeText(doc.name)}`
: escapeText(doc.name ? `**${doc.name}**` : '');
export function getSourceFileMatching(
project: Project,
relativePath: string
): SourceFile | undefined {
return project.getSourceFiles().find((file) => file.getFilePath().indexOf(relativePath) >= 0);
}
78 changes: 78 additions & 0 deletions src/dev/build_api_docs/api_doc_defs/extract_import_refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { getPluginApiDocId } from '../mdx_utils';
import { extractImportReferences } from './extract_import_refs';
import { Reference } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';

const plugins: KibanaPlatformPlugin[] = [getKibanaPlatformPlugin('pluginA', 'plugin_a')];

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_a/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'),
section: 'def-public.Bar',
});
});

it('test extractImportReference with two imports', () => {
const results = extractImportReferences(
`<I extends import("/plugin_a/public/foo").FooFoo, O extends import("/plugin_a/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").FooFoo>`,
plugins,
log
);
expect(results.length).toBe(3);
expect(results[0]).toBe('<I extends ');
expect(results[1]).toBe('FooFoo');
expect(results[2]).toBe('>');
});
82 changes: 82 additions & 0 deletions src/dev/build_api_docs/api_doc_defs/extract_import_refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils';
import { getScopeFromPath } from '../api/utils';
import { getApiSectionId, getPluginApiDocId } from '../mdx_utils';
import { getPluginForPath } from '../plugin_utils';
import { Reference } from '../types';

export function extractImportReferences(
text: string,
plugins: KibanaPlatformPlugin[],
log: ToolingLog
): Array<string | Reference> {
const texts: Array<string | Reference> = [];
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 index = textSegment.indexOf('import("');
texts.push(textSegment.substr(0, index));
const { name, path } = ref;
const lengthOfImport = 'import(".")'.length + path.length + name.length;
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.
texts.push(textSegment.substr(index + lengthOfImport - name.length, name.length));
} else {
const section = getApiSectionId({
pluginName: plugin.manifest.id,
scope: getScopeFromPath(path, log),
apiName: name,
});
texts.push({
docId: getPluginApiDocId(plugin.manifest.id, plugin.manifest.serviceFolders, path),
section,
text: name,
});
}
textSegment = textSegment.substr(index + lengthOfImport);
} else {
texts.push(textSegment);
textSegment = undefined;
}
}
return texts;
}

export function extractImportRef(str: string): { path: string; name: string } | undefined {
const groups = str.match(/import\("(.*?)"\)\.(\w*)/);
if (groups) {
const path = groups[1];
const name = groups[2];
return { path, name };
}
}
Loading

0 comments on commit 5653c59

Please sign in to comment.