Skip to content

Commit

Permalink
refactor(api-markdown-documenter): Allow deeper customization of outp…
Browse files Browse the repository at this point in the history
…ut folder structure (#23366)

Previously, users could control certain aspects of the output
documentation suite's file-system hierarchy via the `documentBoundaries`
and `hierarchyBoundaries` properties of the transformation
configuration.
One particular limitation of this setup was that items yielding
folder-wise hierarchy (`hierarchyBoundaries`) could never place their
own document _inside_ of their own hierarchy.
This naturally lent itself to a pattern where output would commonly be
formatted as:

```
- foo.md
- foo
    - bar.md
    - baz.md
```

This pattern works fine for many site generation systems - a link to
`/foo` will end up pointing `foo.md` and a link to `/foo/bar` will end
up pointing to `foo/bar.md`.
But some systems (e.g. `Docusaurus`) don't handle this well, and instead
prefer setups like the following:

```
- foo
    - index.md
    - bar.md
    - baz.md
```

With the previous configuration options, this pattern was not possible,
but now is.
Additionally, this pattern is _more_ commonly accepted, so lack of
support for this was a real detriment.

Such patterns can now be produced via the consolidated `hierarchy`
property, while still allowing full file-naming flexibility.

### Notes for reviewers

I would recommend starting with `Hierarchy.ts` - it contains the new
configuration options related to hierarchy. The rest of the PR is
predominantly respecting that new configuration setup.

To keep things relatively simple, system defaults and test
configurations have been intentionally made to preserve existing system
default behaviors. As a result, you'll notice that none of the
end-to-end tests have updated collateral. However, the intention _is_ to
update default behaviors, and to add more end-to-end test
configurations. For now, I have left a handful of TODOs in configuration
defaults and test configurations - I will address those in a follow-up
PR.
  • Loading branch information
Josmithr authored Jan 15, 2025
1 parent 091b2df commit 6c2786a
Show file tree
Hide file tree
Showing 28 changed files with 1,528 additions and 971 deletions.
121 changes: 121 additions & 0 deletions tools/api-markdown-documenter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,127 @@ await MarkdownRenderer.renderApiModel({
});
```

#### Update pattern for controlling file-wise hierarchy

Previously, users could control certain aspects of the output documentation suite's file-system hierarchy via the `documentBoundaries` and `hierarchyBoundaries` properties of the transformation configuration.
One particular limitation of this setup was that items yielding folder-wise hierarchy (`hierarchyBoundaries`) could never place their own document _inside_ of their own hierarchy.
This naturally lent itself to a pattern where output would commonly be formatted as:

```
- foo.md
- foo
- bar.md
- baz.md
```

This pattern works fine for many site generation systems - a link to `/foo` will end up pointing `foo.md` and a link to `/foo/bar` will end up pointing to `foo/bar.md`.
But some systems (e.g. `Docusaurus`) don't handle this well, and instead prefer setups like the following:

```
- foo
- index.md
- bar.md
- baz.md
```

With the previous configuration options, this pattern was not possible, but now is.
Additionally, this pattern is _more_ commonly accepted, so lack of support for this was a real detriment.

Such patterns can now be produced via the consolidated `hierarchy` property, while still allowing full file-naming flexibility.

##### Related changes

For consistency / discoverability, the `DocumentationSuiteConfiguration.getFileNameForItem` property has also been moved under the new `hierarchy` property (`HierarchyConfiguration`) and renamed to `getDocumentName`.

Additionally, where previously that property controlled both the document _and_ folder naming corresponding to a given API item, folder naming can now be controlled independently via the `getFolderName` property.

##### Example migration

Consider the following configuration:

```typescript
const config = {
...
documentBoundaries: [
ApiItemKind.Class,
ApiItemKind.Interface,
ApiItemKind.Namespace,
],
hierarchyBoundaries: [
ApiItemKind.Namespace,
]
...
}
```

With this configuration, `Class`, `Interface`, and `Namespace` API items would yield their own documents (rather than being rendered to a parent item's document), and `Namespace` items would additionally generate folder hierarchy (child items rendered to their own documents would be placed under a sub-directory).

Output for this case might look something like the following:

```
- package.md
- class.md
- interface.md
- namespace.md
- namespace
- namespace-member-a.md
- namespace-member-b.md
```

This same behavior can now be configured via the following:

```typescript
const config = {
...
hierarchy: {
[ApiItemKind.Class]: HierarchyKind.Document,
[ApiItemKind.Interface]: HierarchyKind.Document,
[ApiItemKind.Namespace]: {
kind: HierarchyKind.Folder,
documentPlacement: FolderDocumentPlacement.Outside,
},
}
...
}
```

Further, if you would prefer to place the resulting `Namespace` documents _under_ their resulting folder, you could use a configuration like the following:

```typescript
const config = {
...
hierarchy: {
[ApiItemKind.Class]: HierarchyKind.Document,
[ApiItemKind.Interface]: HierarchyKind.Document,
[ApiItemKind.Namespace]: {
kind: HierarchyKind.Folder,
documentPlacement: FolderDocumentPlacement.Inside, // <=
},
getDocumentName: (apiItem) => {
switch(apiItem.kind) {
case ApiItemKind.Namespace:
return "index";
default:
...
}
}
}
...
}
```

Output for this updated case might look something like the following:

```
- package.md
- class.md
- interface.md
- namespace
- index.md
- namespace-member-a.md
- namespace-member-b.md
```

#### Type-renames

- `ApiItemTransformationOptions` -> `ApiItemTransformations`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface ApiItemTransformationConfigurationBase {
}

// @public
export interface ApiItemTransformationOptions extends ApiItemTransformationConfigurationBase, Partial<DocumentationSuiteConfiguration>, LoggingConfiguration {
export interface ApiItemTransformationOptions extends ApiItemTransformationConfigurationBase, DocumentationSuiteOptions, LoggingConfiguration {
readonly defaultSectionLayout?: (apiItem: ApiItem, childSections: SectionNode[] | undefined, config: ApiItemTransformationConfiguration) => SectionNode[];
readonly transformations?: Partial<ApiItemTransformations>;
}
Expand Down Expand Up @@ -106,7 +106,7 @@ export interface ApiItemTransformations {

declare namespace ApiItemUtilities {
export {
doesItemRequireOwnDocument,
createQualifiedDocumentNameForApiItem,
filterItems,
getHeadingForApiItem,
getLinkForApiItem,
Expand Down Expand Up @@ -186,6 +186,9 @@ function createExamplesSection(apiItem: ApiItem, config: ApiItemTransformationCo
// @public
function createParametersSection(apiFunctionLike: ApiFunctionLike, config: ApiItemTransformationConfiguration): SectionNode | undefined;

// @public
function createQualifiedDocumentNameForApiItem(apiItem: ApiItem, hierarchyConfig: HierarchyConfiguration): string;

// @public
function createRemarksSection(apiItem: ApiItem, config: ApiItemTransformationConfiguration): SectionNode | undefined;

Expand All @@ -211,17 +214,22 @@ function createTypeParametersSection(typeParameters: readonly TypeParameter[], c
export const defaultConsoleLogger: Logger;

// @public
export namespace DefaultDocumentationSuiteOptions {
const defaultDocumentBoundaries: ApiMemberKind[];
const defaultHierarchyBoundaries: ApiMemberKind[];
export namespace DefaultDocumentationSuiteConfiguration {
export function defaultGetAlertsForItem(apiItem: ApiItem): string[];
export function defaultGetFileNameForItem(apiItem: ApiItem): string;
export function defaultGetHeadingTextForItem(apiItem: ApiItem): string;
export function defaultGetLinkTextForItem(apiItem: ApiItem): string;
export function defaultGetUriBaseOverrideForItem(): string | undefined;
export function defaultSkipPackage(): boolean;
}

// @public @sealed
export type DocumentationHierarchyConfiguration = SectionHierarchyConfiguration | DocumentHierarchyConfiguration | FolderHierarchyConfiguration;

// @public @sealed
export interface DocumentationHierarchyConfigurationBase {
readonly kind: HierarchyKind;
}

// @public
export interface DocumentationLiteralNode<TValue = unknown> extends Literal<TValue>, DocumentationNode {
readonly isLiteral: true;
Expand Down Expand Up @@ -306,21 +314,26 @@ export abstract class DocumentationParentNodeBase<TDocumentationNode extends Doc

// @public
export interface DocumentationSuiteConfiguration {
readonly documentBoundaries: DocumentBoundaries;
readonly getAlertsForItem: (apiItem: ApiItem) => string[];
readonly getFileNameForItem: (apiItem: ApiItem) => string;
readonly getHeadingTextForItem: (apiItem: ApiItem) => string;
readonly getLinkTextForItem: (apiItem: ApiItem) => string;
readonly getUriBaseOverrideForItem: (apiItem: ApiItem) => string | undefined;
readonly hierarchyBoundaries: HierarchyBoundaries;
readonly hierarchy: HierarchyConfiguration;
readonly includeBreadcrumb: boolean;
readonly includeTopLevelDocumentHeading: boolean;
readonly minimumReleaseLevel: Exclude<ReleaseTag, ReleaseTag.None>;
readonly skipPackage: (apiPackage: ApiPackage) => boolean;
}

// @public
export type DocumentBoundaries = ApiMemberKind[];
export type DocumentationSuiteOptions = Omit<Partial<DocumentationSuiteConfiguration>, "hierarchy"> & {
readonly hierarchy?: HierarchyOptions;
};

// @public @sealed
export interface DocumentHierarchyConfiguration extends DocumentationHierarchyConfigurationBase {
readonly kind: HierarchyKind.Document;
}

// @public
export class DocumentNode implements Parent<SectionNode>, DocumentNodeProps {
Expand Down Expand Up @@ -359,9 +372,6 @@ export namespace DocumentWriter {
export function create(): DocumentWriter;
}

// @public
function doesItemRequireOwnDocument(apiItem: ApiItem, documentBoundaries: DocumentBoundaries): boolean;

// @public
export class FencedCodeBlockNode extends DocumentationParentNodeBase implements MultiLineDocumentationNode {
constructor(children: DocumentationNode[], language?: string);
Expand All @@ -380,6 +390,18 @@ export interface FileSystemConfiguration {
// @public
function filterItems(apiItems: readonly ApiItem[], config: ApiItemTransformationConfiguration): ApiItem[];

// @public
export enum FolderDocumentPlacement {
Inside = "Inside",
Outside = "Outside"
}

// @public @sealed
export interface FolderHierarchyConfiguration extends DocumentationHierarchyConfigurationBase {
readonly documentPlacement: FolderDocumentPlacement;
readonly kind: HierarchyKind.Folder;
}

// @public
export function getApiItemTransformationConfigurationWithDefaults(options: ApiItemTransformationOptions): ApiItemTransformationConfiguration;

Expand Down Expand Up @@ -449,7 +471,39 @@ export class HeadingNode extends DocumentationParentNodeBase<SingleLineDocumenta
}

// @public
export type HierarchyBoundaries = ApiMemberKind[];
export type HierarchyConfiguration = {
/**
* Hierarchy configuration for the API item kind.
*/
readonly [Kind in Exclude<ValidApiItemKind, ApiItemKind.Model | ApiItemKind.EntryPoint | ApiItemKind.Package>]: DocumentationHierarchyConfiguration;
} & {
readonly [ApiItemKind.Model]: DocumentHierarchyConfiguration;
readonly [ApiItemKind.Package]: DocumentHierarchyConfiguration | FolderHierarchyConfiguration;
readonly [ApiItemKind.EntryPoint]: DocumentHierarchyConfiguration;
readonly getDocumentName: (apiItem: ApiItem, config: HierarchyConfiguration) => string;
readonly getFolderName: (apiItem: ApiItem, config: HierarchyConfiguration) => string;
};

// @public
export enum HierarchyKind {
Document = "Document",
Folder = "Folder",
Section = "Section"
}

// @public
export type HierarchyOptions = {
/**
* Hierarchy configuration for the API item kind.
*/
readonly [Kind in Exclude<ValidApiItemKind, ApiItemKind.Model | ApiItemKind.EntryPoint | ApiItemKind.Package>]?: HierarchyKind | DocumentationHierarchyConfiguration;
} & {
readonly [ApiItemKind.Model]?: HierarchyKind.Document | DocumentHierarchyConfiguration;
readonly [ApiItemKind.Package]?: HierarchyKind.Document | HierarchyKind.Folder | DocumentHierarchyConfiguration | FolderHierarchyConfiguration;
readonly [ApiItemKind.EntryPoint]?: HierarchyKind.Document | DocumentHierarchyConfiguration;
readonly getDocumentName?: (apiItem: ApiItem, config: HierarchyConfiguration) => string;
readonly getFolderName?: (apiItem: ApiItem, config: HierarchyConfiguration) => string;
};

// @public
export class HorizontalRuleNode implements MultiLineDocumentationNode {
Expand Down Expand Up @@ -699,6 +753,11 @@ function renderNode(node: DocumentationNode, writer: DocumentWriter, context: Ma
// @public
function renderNodes(children: DocumentationNode[], writer: DocumentWriter, childContext: MarkdownRenderContext): void;

// @public @sealed
export interface SectionHierarchyConfiguration extends DocumentationHierarchyConfigurationBase {
readonly kind: HierarchyKind.Section;
}

// @public
export class SectionNode extends DocumentationParentNodeBase implements MultiLineDocumentationNode {
constructor(children: DocumentationNode[], heading?: HeadingNode);
Expand Down
Loading

0 comments on commit 6c2786a

Please sign in to comment.