Skip to content

Commit

Permalink
feat(api-markdown-documenter): Allow filtering generated docs based o…
Browse files Browse the repository at this point in the history
…n minimal release level (#18663)

This PR adds the `minimalReleaseLevel` configuration option, which
allows consumers to specify a release tag to serve as the "minimal"
level of API items to be processed when generating the API docs suite.

E.g. A user could specify `public` to only generate docs for the public
API surface, or they could specify `beta` to include the API surface
containing beta and public members (but not alpha or internal).

The motivation here is to enable hiding `@alpha` API members from the
public (fluidframework.com) documentation, as those APIs are (currently)
designated for pre-approved Microsoft-internal use only. Our internal
API docs site can continue to show alpha docs.

This PR also updates a couple of the end-to-end test configurations to
leverage this new functionality - you can see the resulting changes in
the test snapshots.

AB#6469
  • Loading branch information
Josmithr authored Dec 6, 2023
1 parent ad605ea commit ee948b5
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 336 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export interface DocumentationSuiteOptions {
hierarchyBoundaries?: HierarchyBoundaries;
includeBreadcrumb?: boolean;
includeTopLevelDocumentHeading?: boolean;
minimumReleaseLevel?: Omit<ReleaseTag, ReleaseTag.None>;
skipPackage?: (apiPackage: ApiPackage) => boolean;
}

Expand Down
2 changes: 1 addition & 1 deletion tools/api-markdown-documenter/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fluid-tools/api-markdown-documenter",
"version": "0.11.1",
"version": "0.11.2",
"description": "Processes .api.json files generated by API-Extractor and generates Markdown documentation from them.",
"homepage": "https://fluidframework.com",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*/
import * as Path from "node:path";

import { ApiItem, ApiItemKind } from "@microsoft/api-extractor-model";
import { type ApiItem, ApiItemKind, ReleaseTag } from "@microsoft/api-extractor-model";

import { Heading } from "../Heading";
import { Link } from "../Link";
import { getQualifiedApiItemName } from "../utilities";
import { getQualifiedApiItemName, getReleaseTag } from "../utilities";
import {
ApiItemTransformationConfiguration,
DocumentBoundaries,
HierarchyBoundaries,
type ApiItemTransformationConfiguration,
type DocumentBoundaries,
type HierarchyBoundaries,
} from "./configuration";

/**
Expand Down Expand Up @@ -459,3 +459,65 @@ function doesItemGenerateHierarchy(
): boolean {
return doesItemKindGenerateHierarchy(apiItem.kind, hierarchyBoundaries);
}

/**
* Determines whether or not the specified API item should have documentation generated for it.
* This is determined based on its release tag (or inherited release scope) compared to
* {@link ApiItemTransformationConfiguration.minimumReleaseLevel}.
*
* @remarks
*
* If an item does not have its own release tag, it will inherit its release scope from its nearest ancestor.
*
* Items without an associated release tag (directly or in their ancestry) will always be included as a precaution.
*
* @param apiItem - The API item being queried.
* @param config - See {@link ApiItemTransformationConfiguration}.
*
* @example Hierarchical inheritance
*
* Items with tagged ancestors inherit their release scope when one is not specified.
* This includes class/interface members...
*
* ```typescript
* // @public
* export interface Foo {
* // `@public` inherited from the interface
* bar: string;
* }
* ```
*
* This also includes scopes like namespaces, which can add further hierarchy...
*
* ```typescript
* // @public
* export namespace Foo {
* // `@public` inherited from the namespace
* export interface Bar {
* // `@public` inherited from the namespace
* baz: string;
* }
* }
* ```
*/
export function shouldItemBeIncluded(
apiItem: ApiItem,
config: Required<ApiItemTransformationConfiguration>,
): boolean {
const releaseTag = getReleaseTag(apiItem);
if (releaseTag === undefined || releaseTag === ReleaseTag.None) {
// If the item does not have a release tag, then it inherits the release scope of its ancestry.
const parent = getFilteredParent(apiItem);
if (parent === undefined) {
// If we encounter an item with no release tag in its ancestry, we can't make a determination as to whether
// or not it is intended to be included in the generated documentation suite.
// To be safe, log a warning but return true.
config.logger.warning("Encountered an API item with no release tag in ancestry.");
return true;
}

return shouldItemBeIncluded(parent, config);
}

return releaseTag >= (config.minimumReleaseLevel as ReleaseTag);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "@microsoft/api-extractor-model";

import { DocumentNode, SectionNode } from "../documentation-domain";
import { doesItemRequireOwnDocument } from "./ApiItemTransformUtilities";
import { doesItemRequireOwnDocument, shouldItemBeIncluded } from "./ApiItemTransformUtilities";
import { createDocument } from "./Utilities";
import { ApiItemTransformationConfiguration } from "./configuration";
import { createBreadcrumbParagraph, wrapInSection } from "./helpers";
Expand Down Expand Up @@ -53,7 +53,7 @@ export function apiItemToDocument(
config: Required<ApiItemTransformationConfiguration>,
): DocumentNode {
if (apiItem.kind === ApiItemKind.None) {
throw new Error(`Encountered API item with a kind of "None".`);
throw new Error(`Encountered API item "${apiItem.displayName}" with a kind of "None".`);
}

if (
Expand All @@ -64,6 +64,12 @@ export function apiItemToDocument(
throw new Error(`Provided API item kind must be handled specially: "${apiItem.kind}".`);
}

if (!shouldItemBeIncluded(apiItem, config)) {
throw new Error(
`Provided API item "${apiItem.displayName}" should not be included in documentation suite per configuration. Cannot generate a document for it.`,
);
}

if (!doesItemRequireOwnDocument(apiItem, config.documentBoundaries)) {
throw new Error(
`"renderApiDocument" called for an API item kind that is not intended to be rendered to its own document. Provided item kind: "${apiItem.kind}".`,
Expand Down Expand Up @@ -105,7 +111,7 @@ export function apiItemToSections(
config: Required<ApiItemTransformationConfiguration>,
): SectionNode[] {
if (apiItem.kind === ApiItemKind.None) {
throw new Error(`Encountered API item with a kind of "None".`);
throw new Error(`Encountered API item "${apiItem.displayName}" with a kind of "None".`);
}

if (
Expand All @@ -116,6 +122,12 @@ export function apiItemToSections(
throw new Error(`Provided API item kind must be handled specially: "${apiItem.kind}".`);
}

if (!shouldItemBeIncluded(apiItem, config)) {
// If a parent item has requested we render contents for an item not desired by the configuration,
// return an empty set of sections.
return [];
}

const logger = config.logger;

logger.verbose(`Rendering section for ${apiItem.displayName}...`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ApiItemTransformationConfiguration,
getApiItemTransformationConfigurationWithDefaults,
} from "./configuration";
import { doesItemRequireOwnDocument } from "./ApiItemTransformUtilities";
import { doesItemRequireOwnDocument, shouldItemBeIncluded } from "./ApiItemTransformUtilities";
import { createBreadcrumbParagraph, createEntryPointList, wrapInSection } from "./helpers";
import { apiItemToDocument, apiItemToSections } from "./TransformApiItem";

Expand Down Expand Up @@ -140,7 +140,10 @@ function getDocumentItems(

const result: ApiItem[] = [];
for (const childItem of apiItem.members) {
if (doesItemRequireOwnDocument(childItem, documentBoundaries)) {
if (
shouldItemBeIncluded(childItem, config) &&
doesItemRequireOwnDocument(childItem, documentBoundaries)
) {
result.push(childItem);
}
result.push(...getDocumentItems(childItem, config));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,29 @@ export interface DocumentationSuiteOptions {
* @defaultValue No front matter is generated.
*/
frontMatter?: string | ((documentItem: ApiItem) => string | undefined);

/**
* Minimal release scope to include in generated documentation suite.
* API members with matching or higher scope will be included, while lower scoped items will be omitted.
*
* @remarks
*
* Note that items tagged as `@internal` are not included in the models generated by API-Extractor,
* so `@internal` items will never be included for such models.
*
* Hierarchy: `@public` \> `@beta` \> `@alpha` \> `@internal`
*
* @defaultValue Include everything in the input model.
*
* @example `@beta` and `@public`
*
* To only include `@beta` and `@public` items (and omit `@alpha` items), specify:
*
* ```typescript
* releaseLevel: ReleaseTag.Beta
* ```
*/
minimumReleaseLevel?: Omit<ReleaseTag, ReleaseTag.None>;
}

/**
Expand Down Expand Up @@ -359,6 +382,7 @@ const defaultDocumentationSuiteOptions: Required<DocumentationSuiteOptions> = {
getLinkTextForItem: DefaultDocumentationSuiteOptions.defaultGetLinkTextForItem,
skipPackage: DefaultDocumentationSuiteOptions.defaultSkipPackage,
frontMatter: DefaultDocumentationSuiteOptions.defaultFrontMatter,
minimumReleaseLevel: ReleaseTag.Internal, // Include everything in the input model
};

/**
Expand Down
4 changes: 3 additions & 1 deletion tools/api-markdown-documenter/src/test/HtmlEndToEnd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import * as Path from "node:path";

import { ApiItemKind, ApiModel } from "@microsoft/api-extractor-model";
import { ApiItemKind, ApiModel, ReleaseTag } from "@microsoft/api-extractor-model";
import { FileSystem } from "@rushstack/node-core-library";
import { expect } from "chai";
import { Suite } from "mocha";
Expand Down Expand Up @@ -186,6 +186,7 @@ describe("HTML rendering end-to-end tests", () => {
hierarchyBoundaries: [], // No additional hierarchy beyond the package level
frontMatter: (documentItem): string =>
`<!--- This is sample front-matter for API item "${documentItem.displayName}" -->`,
minimumReleaseLevel: ReleaseTag.Beta, // Only include `@public` and `beta` items in the docs suite
},
renderConfig: {},
},
Expand Down Expand Up @@ -219,6 +220,7 @@ describe("HTML rendering end-to-end tests", () => {
ApiItemKind.Variable,
],
hierarchyBoundaries: [], // No additional hierarchy beyond the package level
minimumReleaseLevel: ReleaseTag.Public, // Only include `@public` items in the docs suite
},
renderConfig: {
startingHeadingLevel: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import * as Path from "node:path";

import { ApiItemKind, ApiModel } from "@microsoft/api-extractor-model";
import { ApiItemKind, ApiModel, ReleaseTag } from "@microsoft/api-extractor-model";
import { FileSystem } from "@rushstack/node-core-library";
import { expect } from "chai";
import { Suite } from "mocha";
Expand Down Expand Up @@ -184,6 +184,7 @@ describe("Markdown rendering end-to-end tests", () => {
hierarchyBoundaries: [], // No additional hierarchy beyond the package level
frontMatter: (documentItem): string =>
`<!--- This is sample front-matter for API item "${documentItem.displayName}" -->`,
minimumReleaseLevel: ReleaseTag.Beta, // Only include `@public` and `beta` items in the docs suite
},
renderConfig: {},
},
Expand Down Expand Up @@ -217,6 +218,7 @@ describe("Markdown rendering end-to-end tests", () => {
ApiItemKind.Variable,
],
hierarchyBoundaries: [], // No additional hierarchy beyond the package level
minimumReleaseLevel: ReleaseTag.Public, // Only include `@public` items in the docs suite
},
renderConfig: {
startingHeadingLevel: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2578,105 +2578,6 @@ <h3 id="typealias-remarks">
<h1>
Function Details
</h1>
<section>
<h2 id="testfunction-function">
testFunction (ALPHA)
</h2>
<section>
<p>
Test function
</p>
</section>
<section>
<span><b>WARNING: This API is provided as an alpha preview and may change without notice. Use at your own risk.</b></span>
</section>
<section>
<h3 id="testfunction-signature">
Signature
</h3>
<code>
export declare function testFunction&lt;TTypeParameter&gt;(testParameter: TTypeParameter, testOptionalParameter?: TTypeParameter): TTypeParameter;
</code>
</section>
<section>
<h3 id="testfunction-remarks">
Remarks
</h3>
<p>
This is a test <a href='docs/simple-suite-test#testinterface-interface'>link</a> to another API member
</p>
</section>
<section>
<h3 id="testfunction-parameters">
Parameters
</h3>
<table>
<thead>
<tr>
<th>
Parameter
</th>
<th>
Modifiers
</th>
<th>
Type
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
testParameter
</td>
<td>
</td>
<td>
<span>TTypeParameter</span>
</td>
<td>
A test parameter
</td>
</tr>
<tr>
<td>
testOptionalParameter
</td>
<td>
optional
</td>
<td>
<span>TTypeParameter</span>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h3 id="testfunction-returns">
Returns
</h3>
<p>
The provided parameter
</p>
<p>
<span><b>Return type:</b> </span><span>TTypeParameter</span>
</p>
</section>
<section>
<h3 id="testfunction-throws">
Throws
</h3>
<p>
An Error when something bad happens.
</p>
</section>
</section>
<section>
<h2 id="testfunctionreturninginlinetype-function">
testFunctionReturningInlineType
Expand Down
Loading

0 comments on commit ee948b5

Please sign in to comment.