Skip to content
Merged
69 changes: 69 additions & 0 deletions website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,75 @@ For an overview of how to leverage React components in MDX documentation, see [h
React components should be saved under `src/components/...`.
They can be imported in other components, pages, and documents via `@site/src/components/...`.

##### Linking to API documentation

Use `PackageLink` and `ApiLink` to link to generated API documentation from an MDX document.
These components select the API documentation for the active documentation version.

Import the components that the document uses:

```tsx
import { ApiLink, PackageLink } from "@site/src/components/shortLinks";
```

Use the unscoped package name with both components.
Use a TSDoc declaration reference for the `api` value of `ApiLink`:

```mdx
See <PackageLink package="fluid-framework" /> and <ApiLink package="fluid-framework" api="TreeView.upgradeSchema" />.
```

Add `newApi` when the current published API model does not contain a new package or API.
The component renders its content as inline code until the target exists.
The component writes a build debug message when it renders inline code.
When the target exists, the component renders a link.
`newApi` can be removed once the target exists.

```mdx
<PackageLink package="new-package" newApi />

<ApiLink package="fluid-framework" api="NewApi" newApi />
```

The `newApi` shorthand is equivalent to `newApi={true}`.
Invalid references and ambiguous references remain build errors.
Use a TSDoc selector to resolve an ambiguous API kind, such as `(NewApi:class)` or `(NewApi:interface)`.

For an API rename, set `api` to an object with `previous` and `new` declaration references:

```mdx
<ApiLink package="fluid-framework" api={{ previous: "OldApi", new: "(NewApi:class)" }} />
```

The component tries `new` first.
It uses `previous` while the published model contains only the old API.
When child content is omitted, the component displays the name of the API that resolves.

```mdx
<ApiLink package="fluid-framework" api="(NewApi:class)" />
```

For a package rename, set `package` to an object with `previous` and `new` names:

```mdx
<PackageLink package={{ previous: "old-package", new: "new-package" }} />
```

The component uses `previous` while the published model contains only the old package.

```mdx
<PackageLink package="new-package" />
```

All transition modes permit explicit rich child content.
The component preserves the child content when it renders a link or inline code:

```mdx
<ApiLink package="fluid-framework" api="NewApi" newApi>
**New API**
</ApiLink>
```

#### Comments

A common pattern for adding inline comments in `.md` files looks like:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ const config = new TreeViewConfiguration({ schema: Board });
Once a document's stored schema has been upgraded, it cannot be downgraded.
However, rolling back a feature flag is safe.
With the feature flag rolled back, new documents and any documents that haven't been upgraded yet will not have the upgrade enabled.
{/* TODO: link directly to `isStagedUpgradeEnabled` property once it has been released. */}
You can use <ApiLink package="fluid-framework" api="(TreeViewAlpha:interface)"/>'s `isStagedUpgradeEnabled` property to check whether a document has already been upgraded and conditionally include that upgrade token for those documents.
You can use <ApiLink package="fluid-framework" api="(TreeViewAlpha:interface).isStagedUpgradeEnabled" newApi/> to check whether a document has already been upgraded and conditionally include that upgrade token for those documents.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: this is an example of one of the classes of problems this PR is aimed at addressing. We added docs for a new property that hadn't yet been released and initially tried to link to it, which caused the website build to fail. We can now "stage" the link, which will go live as soon as the API has been released.


Disabling the feature flag prevents **new** documents from being upgraded, but documents that have already been upgraded retain the new schema.
This means:
Expand Down
49 changes: 43 additions & 6 deletions website/src/apiLinkReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ export interface ResolvedApiLink {
readonly defaultText: string;
}

/**
* The result of trying to resolve an API declaration reference that is not documented.
*/
export interface UnresolvedApiLink {
/**
* Indicates that the API declaration reference did not resolve.
*/
readonly found: false;

/**
* The declaration's dotted member path with selector syntax omitted.
*/
readonly defaultText: string;
}

/**
* The result of trying to resolve an API declaration reference.
*/
export type ApiLinkResolution = (ResolvedApiLink & { readonly found: true }) | UnresolvedApiLink;

/**
* Resolves an API declaration reference from one version's API link manifest.
*/
Expand All @@ -193,11 +213,28 @@ export function resolveApiLinkTarget(
packageName: string,
api: string,
): ResolvedApiLink {
const result = tryResolveApiLinkTarget(manifest, packageName, api);
if (!result.found) {
throw new Error(`No API documentation found for "${packageName}/${api}".`);
}
return { target: result.target, defaultText: result.defaultText };
}

/**
* Tries to resolve an API declaration reference from one version's API link manifest.
*
* @remarks Parsing errors, unsupported selectors, and ambiguous references remain errors.
*/
export function tryResolveApiLinkTarget(
manifest: Readonly<ApiLinkManifest>,
packageName: string,
api: string,
): ApiLinkResolution {
const referencePath = parseApiReference(api);
const apiName = referencePath.map((segment) => segment.name).join(".");
const candidates = manifest[packageName]?.[apiName];
if (candidates === undefined) {
throw new Error(`No API documentation found for "${packageName}/${api}".`);
return { found: false, defaultText: apiName };
}

let matchingCandidates = candidates.filter((candidate) =>
Expand All @@ -207,7 +244,7 @@ export function resolveApiLinkTarget(
),
);
if (matchingCandidates.length === 0) {
throw new Error(`No API documentation found for "${packageName}/${api}".`);
return { found: false, defaultText: apiName };
}

const requestedOverload = referencePath.at(-1)?.overloadIndex;
Expand All @@ -216,7 +253,7 @@ export function resolveApiLinkTarget(
(candidate) => candidate.path.at(-1)?.overloadIndex === requestedOverload,
);
if (matchingCandidates.length === 0) {
throw new Error(`No API documentation found for "${packageName}/${api}".`);
return { found: false, defaultText: apiName };
}
}

Expand All @@ -241,15 +278,15 @@ export function resolveApiLinkTarget(
(candidate) => candidate.path.at(-1)?.overloadIndex === 1,
);
if (overloadOne !== undefined) {
return { target: overloadOne, defaultText: apiName };
return { found: true, target: overloadOne, defaultText: apiName };
}
}

const target = matchingCandidates[0];
if (target === undefined) {
throw new Error(`No API documentation found for "${packageName}/${api}".`);
return { found: false, defaultText: apiName };
}
return { target, defaultText: apiName };
return { found: true, target, defaultText: apiName };
}

function parseApiReference(api: string): readonly ApiReferenceSegment[] {
Expand Down
Loading
Loading