From 2830625659ac3b34e95ae696051943ed7180a5ce Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Wed, 2 Sep 2026 00:18:34 +0000
Subject: [PATCH 01/11] docs: Add design doc
---
website/STAGED_API_LINKS_DESIGN.md | 463 +++++++++++++++++++++++++++++
1 file changed, 463 insertions(+)
create mode 100644 website/STAGED_API_LINKS_DESIGN.md
diff --git a/website/STAGED_API_LINKS_DESIGN.md b/website/STAGED_API_LINKS_DESIGN.md
new file mode 100644
index 00000000000..e778b1cea70
--- /dev/null
+++ b/website/STAGED_API_LINKS_DESIGN.md
@@ -0,0 +1,463 @@
+# API link transitions design
+
+## Summary
+
+Add two transition mechanisms to `PackageLink` and `ApiLink`:
+
+- A `newApi` prop permits a target to be absent from the active API documentation. The component renders inline code when the target is absent. When the target exists, the component renders a link and logs a warning that tells the author to remove `newApi`.
+- A `replacementApi` prop gives `ApiLink` a preferred API reference. The component tries this reference before the reference in `api`. It uses `api` as a fallback while published API documentation still contains the old name. When `replacementApi` resolves, the component logs a warning that tells the author to make the new reference permanent.
+- A `replacementPackage` prop gives `PackageLink` the same rename behavior for package names.
+
+These mechanisms let a website change merge with its related API change. The website can then work before and after release artifacts contain the API change.
+
+## Background
+
+The website publishes from the `main` branch. Published API documentation models come from release branches. Therefore, website content on `main` can be newer than the API documentation that the website consumes.
+
+A link to a new API fails until a release branch publishes an API model that contains the API. A link update for a renamed API has the inverse problem. The old name works before the new model is published, and the new name works after publication. There is no single current `ApiLink` value that works in both states.
+
+## Goals
+
+The design has these goals:
+
+1. Permit documentation for a new package or API to merge before its API model is published.
+2. Permit documentation for a renamed API to work with both the old and new API models.
+3. Keep normal API links strict.
+4. Keep invalid declaration references and ambiguous references as build errors.
+5. Tell authors when a temporary transition prop is no longer necessary.
+6. Resolve all links against the active documentation version.
+7. Preserve the current output for existing component use.
+
+## Non-goals
+
+The design does not do these tasks:
+
+- It does not change API model publication or website deployment.
+- It does not permit arbitrary broken links.
+- It does not add a fallback URL supplied by an author.
+- It does not find renamed APIs automatically.
+- It does not validate an arbitrary `headingId` in `PackageLink`.
+- It does not make transition warnings fail the build.
+
+## Proposed component API
+
+### `PackageLink`
+
+Add an optional `newApi` prop:
+
+```tsx
+export interface PackageLinkProps {
+ children?: ReactNode;
+ package: string;
+ replacementPackage?: string;
+ headingId?: string;
+ newApi?: boolean;
+}
+```
+
+An MDX file can link to a package that is not in the published model:
+
+```mdx
+
+```
+
+The package name remains unscoped. This rule is the same as the current rule.
+
+An MDX file can prepare for a package rename:
+
+```mdx
+
+```
+
+`replacementPackage` contains the preferred unscoped package name. The `package` prop remains the fallback name. When the transition is complete, the author copies the `replacementPackage` value to `package` and removes `replacementPackage`.
+
+### `ApiLink`
+
+Add optional `newApi` and `replacementApi` props:
+
+```tsx
+export interface ApiLinkProps<
+ TApiSelector extends string = string,
+ TReplacementApiSelector extends string = string,
+> {
+ children?: ReactNode;
+ package: string;
+ api: ApiDeclarationReference;
+ replacementApi?: ApiDeclarationReference;
+ newApi?: boolean;
+ headingId?: string;
+}
+```
+
+An MDX file can refer to a new API:
+
+```mdx
+
+```
+
+Because `newApi` has the `boolean` type, MDX and JSX permit the shorthand `newApi`. This syntax is equivalent to `newApi={true}`. The shorthand is the preferred syntax.
+
+An MDX file can prepare for a rename:
+
+```mdx
+
+```
+
+`replacementApi` uses the same declaration-reference grammar as `api`. An author specifies an API item kind with a TSDoc selector. For example, `(NewApi:class)` selects a class and `(NewApi:interface).method` selects a method on an interface. A separate API-kind prop is not necessary.
+
+The `api` prop remains the fallback reference. When the transition is complete, the author copies the `replacementApi` value to `api` and removes `replacementApi`.
+
+## Resolution behavior
+
+All checks use the manifest for the active Docusaurus documentation version. A target can exist in one documentation version and be absent from another version.
+
+### `PackageLink` behavior
+
+A normal `PackageLink` keeps its current behavior. It creates the package URL without a manifest lookup.
+
+A `PackageLink` with `replacementPackage` checks the replacement package first. If the replacement package does not exist, it uses `package`. When the replacement package exists, the component links to it and logs a warning to make the replacement permanent.
+
+The following table applies when `replacementPackage` is not present:
+
+| `newApi` | Package exists | Result |
+| -------- | -------------- | ----------------------------------------------------------- |
+| Not set | Either state | Render the current link. Existing broken-link checks apply. |
+| `true` | No | Render the content as inline code. Do not throw. |
+| `true` | Yes | Render the link. Log a warning to remove `newApi`. |
+
+The following table applies when `replacementPackage` is present:
+
+| Replacement package exists | Original package exists | `newApi` | Result |
+| -------------------------- | ----------------------- | ------------ | ----------------------------------------------------------------------------------------------- |
+| Yes | Either state | Either state | Link to `replacementPackage`. Log a warning to replace `package` and remove the temporary prop. |
+| No | Yes | Either state | Link to `package`. Do not log a transition warning. |
+| No | No | `true` | Render inline code. Do not throw. |
+| No | No | Not set | Render the current original-package link. Existing broken-link checks apply. |
+
+If `replacementPackage` and `newApi` are both present, replacement resolution runs first. `newApi` controls only the final state in which neither package exists.
+
+The manifest identifies package existence. It does not contain package-heading metadata. Therefore, `headingId` continues to use the normal Docusaurus anchor check.
+
+### `ApiLink` behavior
+
+`ApiLink` resolves `replacementApi` first when that prop is present. If the preferred reference is not found, it resolves `api`.
+
+| `replacementApi` target | `api` target | `newApi` | Result |
+| --------------------------------- | -------------- | ------------ | ------------------------------------------------------------------------------------- |
+| Exists | Either state | Either state | Link to `replacementApi`. Log a warning to replace `api` and remove `replacementApi`. |
+| Does not exist | Exists | Either state | Link to `api`. Do not log a transition warning. |
+| Not supplied | Exists | `true` | Link to `api`. Log a warning to remove `newApi`. |
+| Not supplied | Exists | Not set | Render the current link. |
+| Does not exist or is not supplied | Does not exist | `true` | Render inline code. Do not throw. |
+| Does not exist or is not supplied | Does not exist | Not set | Throw the current not-found error. |
+
+If both `replacementApi` and `newApi` are present, replacement resolution runs first. `newApi` only controls the final state in which neither reference exists. This combination supports a new API that changes name before its first API documentation artifact is published.
+
+### Errors that remain strict
+
+A transition prop suppresses only a not-found result. The following conditions remain errors:
+
+- The declaration-reference syntax is invalid.
+- A selector is not supported.
+- A reference is ambiguous.
+- The component is outside a versioned Docusaurus document.
+- The active documentation version has no manifest.
+
+This rule prevents `newApi` from hiding author errors. A kind or overload selector that is valid but has no matching target is a not-found result. This result can use the transition behavior.
+
+## Display text
+
+The fallback output is a React `` element. This element uses the same inline-code styling as inline code in MDX. The component does not add literal backtick characters.
+
+Each component preserves explicit `children`, including rich React content:
+
+```tsx
+{children ?? defaultText}
+```
+
+For `PackageLink` without `replacementPackage`, `defaultText` is the package name.
+
+For `PackageLink` with `replacementPackage`, `defaultText` is always the replacement package name. This rule applies when the component links to the original package.
+
+For `ApiLink` without `replacementApi`, `defaultText` is the selector-free dotted path from `api`. This behavior matches the current link text.
+
+For `ApiLink` with `replacementApi`, `defaultText` is always the selector-free dotted path from `replacementApi`. This rule applies when the component links to the old target. It lets the visible documentation use the new API name before the new API documentation exists.
+
+For example, this source:
+
+```mdx
+
+```
+
+renders visible text `NewApi` in both transition states. Before publication, the link points to `OldApi`. After publication, the link points to `NewApi`.
+
+All transition states permit rich child content. The component puts the same child tree in the link or the inline-code fallback. Authors can use inline elements such as emphasis and API display formatting. Authors should not add an outer inline-code element because the fallback already creates one.
+
+## Resolution API changes
+
+The current `resolveApiLinkTarget` function throws the same not-found error for these cases:
+
+- The package or dotted path is absent.
+- A valid kind selector has no match.
+- A valid overload selector has no match.
+
+The component must distinguish these cases from invalid or ambiguous references. Do not inspect error-message text.
+
+Add a non-throwing not-found result to the resolver layer. One possible shape is:
+
+```ts
+export type ApiLinkResolution =
+ | {
+ readonly found: true;
+ readonly target: ApiLinkManifestEntry;
+ readonly defaultText: string;
+ }
+ | {
+ readonly found: false;
+ readonly defaultText: string;
+ };
+
+export function tryResolveApiLinkTarget(
+ manifest: Readonly,
+ packageName: string,
+ api: string,
+): ApiLinkResolution;
+```
+
+`tryResolveApiLinkTarget` parses and validates the reference first. It throws for invalid, unsupported, or ambiguous references. It returns `found: false` only when no candidate matches a valid reference.
+
+Keep `resolveApiLinkTarget` as the strict wrapper:
+
+```ts
+export function resolveApiLinkTarget(/* current parameters */): ResolvedApiLink {
+ const result = tryResolveApiLinkTarget(/* current arguments */);
+ if (!result.found) {
+ throw new Error(/* current not-found message */);
+ }
+ return result;
+}
+```
+
+This structure preserves the existing strict function and its error messages. It also gives the components a typed result for transition behavior.
+
+The parser must expose selector-free `defaultText` even for a not-found reference. This data is necessary for new-API output and rename display text.
+
+## Warning behavior
+
+Use `console.warn` during server rendering. Docusaurus includes server-render output in local and continuous integration build logs.
+
+Warnings should use stable and actionable text. Proposed messages are:
+
+```text
+[PackageLink] Package "new-package" exists in API documentation version "current". Remove the newApi prop.
+```
+
+```text
+[ApiLink] API "fluid-framework/NewApi" exists in API documentation version "current". Remove the newApi prop.
+```
+
+```text
+[ApiLink] Replacement API "fluid-framework/NewApi" exists in API documentation version "current". Set api="NewApi" and remove the replacementApi prop.
+```
+
+```text
+[PackageLink] Replacement package "new-package" exists in API documentation version "current". Set package="new-package" and remove the replacementPackage prop.
+```
+
+The actual message must preserve selector syntax when it tells the author which value to put in `api`.
+
+Use a module-level `Set` to reduce duplicate warnings in one process. Include these values in the warning key:
+
+- Component type
+- Active documentation version
+- Package name
+- API reference, when applicable
+- Transition prop type
+
+Only emit these warnings when `typeof window === "undefined"`. This rule prevents every reader from receiving author diagnostics in the browser console. Parallel Docusaurus workers can still produce more than one copy of a warning. Duplicate prevention is best effort and must not affect correctness.
+
+Warnings are best-effort build messages. They must not fail the build. A later change can add a stricter cleanup mechanism if the team needs one.
+
+## `headingId` behavior
+
+`ApiLink.headingId` is deprecated. The new resolution order does not change that status.
+
+When `headingId` is present, it overrides the generated heading ID for the target that wins API resolution. Authors should not use it for an API replacement transition because one override might not be correct for both targets. Qualified declaration references remain the supported solution.
+
+`PackageLink.headingId` keeps its current behavior. One heading ID must work with both package targets during a package replacement transition.
+
+## Implementation plan
+
+### 1. Refactor API reference resolution
+
+Update `website/src/apiLinkReference.ts`:
+
+1. Separate reference parsing from candidate lookup.
+2. Add `tryResolveApiLinkTarget` and `ApiLinkResolution`.
+3. Return selector-free display text for found and not-found results.
+4. Keep `resolveApiLinkTarget` as a strict compatibility wrapper.
+5. Keep current ambiguity, parser, and selector errors.
+
+### 2. Share active-version manifest lookup
+
+Update `website/src/components/shortLinks.tsx`:
+
+1. Add a helper that gets the active version and its manifest.
+2. Use this helper in `ApiLink`.
+3. Use this helper in `PackageLink` only when `newApi` is true.
+4. Preserve the current normal `PackageLink` path construction.
+
+The helper must keep the current errors for a missing version context and a missing manifest. Error text can name the calling component.
+
+### 3. Add transition rendering
+
+Update `PackageLink` and `ApiLink`:
+
+1. Add the new props and TSDoc comments.
+2. Implement the resolution tables in this document.
+3. Render `` for the permitted final not-found state.
+4. Keep explicit children unchanged.
+5. Use the preferred replacement reference or package name for default display text.
+6. Preserve rich child content in link and inline-code output.
+
+### 4. Add warning support
+
+Add a small private warning helper in `shortLinks.tsx`, or in a nearby module if tests need direct access. The helper emits server-only, best-effort deduplicated warnings.
+
+Do not add warning data to the generated manifest. The warning depends on component props, not on API model metadata.
+
+### 5. Add documentation examples
+
+Add contributor guidance for these props after implementation. State that both props are temporary. Include the required cleanup operation for each warning.
+
+Do not add a new-API example to published product documentation only to test the component. Use unit tests for transition states.
+
+## Test plan
+
+Extend `website/test/unit/shortLinks.test.ts` and the resolver unit tests.
+
+### Type tests
+
+Verify these cases:
+
+- `newApi` is accepted by both components with JSX boolean shorthand.
+- `replacementApi` accepts a valid dotted reference.
+- `replacementApi` accepts a kind selector.
+- `replacementApi` rejects an invalid literal reference.
+- `replacementPackage` accepts a string.
+- Existing `api` type checks continue to work.
+
+### `PackageLink` tests
+
+Verify these cases:
+
+- Normal links keep their current output and do not require manifest data.
+- A missing new package renders `` with default text when `newApi` is present.
+- A missing new package preserves explicit children when `newApi` is present.
+- An existing package renders a link when `newApi` is present.
+- An existing package logs the cleanup warning when `newApi` is present.
+- The warning includes the active version.
+- A missing `replacementPackage` target falls back to `package` without a warning.
+- An existing `replacementPackage` target wins and logs a cleanup warning.
+- Default text uses `replacementPackage` before and after the target exists.
+- Explicit rich children remain unchanged before and after the target exists.
+- If both package names are absent, `newApi` controls code output versus the current original-package link.
+
+### `ApiLink` tests
+
+Verify these cases:
+
+- A missing API renders `` when `newApi` is present.
+- A missing API preserves explicit children when `newApi` is present.
+- An existing API renders a link and logs the cleanup warning when `newApi` is present.
+- A missing `replacementApi` target falls back to `api` without a warning.
+- An existing `replacementApi` target wins and logs a cleanup warning.
+- Default text uses `replacementApi` before and after the target exists.
+- Explicit rich children remain unchanged before and after the target exists.
+- If both references are absent, `newApi` controls code output versus an error.
+- A malformed preferred reference throws and does not fall back.
+- An ambiguous preferred reference throws and does not fall back.
+- A valid preferred selector with no match falls back.
+- A valid preferred overload with no match falls back.
+- Existing version, anchor, overload, and selector tests continue to pass.
+
+### Warning tests
+
+Mock `console.warn` and server rendering. Reset the warning-key set between tests through a test-only reset function or module reset. Verify warning content and deduplication.
+
+Do not depend only on browser tests for warnings. The warnings are server diagnostics.
+
+### Build validation
+
+Run these website checks:
+
+1. Unit tests.
+2. TypeScript test compilation.
+3. ESLint and Prettier checks.
+4. A full Docusaurus build with generated API documentation.
+
+The full build confirms that `` output works in MDX server rendering and that warnings appear in build logs.
+
+## Compatibility
+
+The new props are optional. Existing MDX source and rendered output do not change.
+
+The strict `resolveApiLinkTarget` export keeps its current behavior. New code uses `tryResolveApiLinkTarget` only when it needs transition behavior.
+
+The generated manifest format does not change. Existing generated manifests remain valid.
+
+The feature works independently for each active documentation version. A link with `newApi` can render as code in one version and as a link in another version. A warning identifies the version in which cleanup is possible.
+
+## Alternatives considered
+
+### Catch all resolver errors in the component
+
+This option is small, but it can hide malformed and ambiguous references. It also depends on thrown error text to identify not-found results. Reject this option.
+
+### Add a replacement URL
+
+An author-supplied URL bypasses manifest validation and version handling. It can become stale. Reject this option.
+
+### Put replacement aliases in the generated manifest
+
+The API models do not contain the documentation author's old-to-new mapping. An alias file would add a separate data source and cleanup process. A component prop keeps the transition next to the affected content. Reject this option for the first implementation.
+
+### Render plain text instead of inline code
+
+Plain text does not identify an API symbol as clearly as existing MDX inline code. The requested fallback is inline code. Reject this option.
+
+### Make a successful new-API link fail the build
+
+A build error would force immediate cleanup, but artifact updates can occur without a matching website change. This behavior would recreate the current urgent failure. Use a warning instead.
+
+## Cleanup workflow
+
+For a new API or package:
+
+1. Add the link with `newApi` in the website change that documents the new item.
+2. The website renders inline code until the artifact contains the item.
+3. A later website build logs a warning after the artifact contains the item.
+4. Remove `newApi` in a cleanup change.
+
+For a renamed API:
+
+1. Keep the old reference in `api`.
+2. Put the new qualified reference in `replacementApi`.
+3. The website displays the new name and links to the old API documentation before publication.
+4. The website links to the new API documentation and logs a warning after publication.
+5. Copy the `replacementApi` value to `api` and remove `replacementApi`.
+
+For a renamed package:
+
+1. Keep the old package name in `package`.
+2. Put the new unscoped package name in `replacementPackage`.
+3. The website displays the new package name and links to the old package documentation before publication.
+4. The website links to the new package documentation and logs a warning after publication.
+5. Copy the `replacementPackage` value to `package` and remove `replacementPackage`.
+
+## Resolved design decisions
+
+1. Use `replacementApi` for API rename transitions.
+2. Use best-effort build warnings. Do not add stricter cleanup enforcement now.
+3. Support package rename transitions with `replacementPackage` in the first implementation.
+4. Permit rich child content in all link and fallback states.
From be0e8623120605bad555e31e035c89e45571383a Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Wed, 2 Sep 2026 17:18:38 +0000
Subject: [PATCH 02/11] feat(website): Add staged / renamed API support in
shortlinks
---
website/README.md | 67 +++++++++
website/STAGED_API_LINKS_DESIGN.md | 4 +-
website/src/apiLinkReference.ts | 49 ++++++-
website/src/components/shortLinks.tsx | 192 +++++++++++++++++++++-----
website/test/unit/shortLinks.test.ts | 176 ++++++++++++++++++++++-
5 files changed, 448 insertions(+), 40 deletions(-)
diff --git a/website/README.md b/website/README.md
index 4234180a6b0..208da00ce27 100644
--- a/website/README.md
+++ b/website/README.md
@@ -157,6 +157,73 @@ 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 and .
+```
+
+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.
+When the target exists, the component renders a link and writes a build warning.
+Remove `newApi` when this warning occurs.
+
+```mdx
+
+
+
+```
+
+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)`.
+
+Use `replacementApi` when an API is renamed.
+Keep the old reference in `api` and put the new reference in `replacementApi`:
+
+```mdx
+
+```
+
+The component tries `replacementApi` first.
+It uses `api` while the published model contains only the old API.
+The visible default text uses the replacement name in both states.
+When the replacement exists, the component writes a build warning.
+Copy the `replacementApi` value to `api`, and then remove `replacementApi`.
+
+Use `replacementPackage` in the same way for a package rename:
+
+```mdx
+
+```
+
+When the replacement package exists, copy its name to `package`, and then remove `replacementPackage`.
+
+All transition modes permit explicit rich child content.
+The component preserves the child content when it renders a link or inline code:
+
+```mdx
+
+ **New API**
+
+```
+
+Transition behavior is version-specific.
+A component can render inline code or use its old target in one documentation version and use its new target in another version.
+For implementation details and the complete behavior tables, see the [API link transitions design](./STAGED_API_LINKS_DESIGN.md).
+
#### Comments
A common pattern for adding inline comments in `.md` files looks like:
diff --git a/website/STAGED_API_LINKS_DESIGN.md b/website/STAGED_API_LINKS_DESIGN.md
index e778b1cea70..c6327122c2b 100644
--- a/website/STAGED_API_LINKS_DESIGN.md
+++ b/website/STAGED_API_LINKS_DESIGN.md
@@ -43,7 +43,7 @@ The design does not do these tasks:
### `PackageLink`
-Add an optional `newApi` prop:
+Add optional `newApi` and `replacementPackage` props:
```tsx
export interface PackageLinkProps {
@@ -304,7 +304,7 @@ Update `website/src/components/shortLinks.tsx`:
1. Add a helper that gets the active version and its manifest.
2. Use this helper in `ApiLink`.
-3. Use this helper in `PackageLink` only when `newApi` is true.
+3. Use this helper in `PackageLink` when `newApi` is true or `replacementPackage` is present.
4. Preserve the current normal `PackageLink` path construction.
The helper must keep the current errors for a missing version context and a missing manifest. Error text can name the calling component.
diff --git a/website/src/apiLinkReference.ts b/website/src/apiLinkReference.ts
index 09ed2511ecf..b7e41324c74 100644
--- a/website/src/apiLinkReference.ts
+++ b/website/src/apiLinkReference.ts
@@ -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.
*/
@@ -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,
+ 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) =>
@@ -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;
@@ -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 };
}
}
@@ -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[] {
diff --git a/website/src/components/shortLinks.tsx b/website/src/components/shortLinks.tsx
index 92b5f03173b..b6f354ede49 100644
--- a/website/src/components/shortLinks.tsx
+++ b/website/src/components/shortLinks.tsx
@@ -3,15 +3,18 @@
* Licensed under the MIT License.
*/
-import { useActivePluginAndVersion } from "@docusaurus/plugin-content-docs/client";
+import {
+ type GlobalVersion,
+ useActivePluginAndVersion,
+} from "@docusaurus/plugin-content-docs/client";
import { usePluginData } from "@docusaurus/useGlobalData";
import type { ReactNode } from "react";
import { type ApiLinkManifests, apiLinkManifestPluginName } from "../apiLinkManifest";
-import { type ApiDeclarationReference, resolveApiLinkTarget } from "../apiLinkReference";
+import { type ApiDeclarationReference, tryResolveApiLinkTarget } from "../apiLinkReference";
import type { SiteVersion } from "../utilityTypes";
-// TODO: how will versioning interact with these?
+const emittedTransitionWarnings = new Set();
/**
* {@link PackageLink} input props.
@@ -28,7 +31,22 @@ export interface PackageLinkProps {
*/
package: string;
+ /**
+ * The new unscoped package name after a package rename.
+ *
+ * @remarks Remove this prop and update {@link PackageLinkProps.package} when the new package API
+ * documentation is available.
+ */
+ replacementPackage?: string;
+
headingId?: string;
+
+ /**
+ * Permits the package to be absent from the published API documentation.
+ *
+ * @remarks Remove this prop when the package API documentation is available.
+ */
+ newApi?: boolean;
}
/**
@@ -37,17 +55,59 @@ export interface PackageLinkProps {
export function PackageLink({
headingId,
package: packageName,
+ replacementPackage,
children,
+ newApi = false,
}: PackageLinkProps): JSX.Element {
- const root = useLinkPathBase();
+ const needsManifest = newApi || replacementPackage !== undefined;
+ const { activeVersion, manifest } = useApiLinkContext("PackageLink", needsManifest);
+ const root = `${activeVersion.path}/api/`;
const headingPostfix = headingId === undefined ? "" : `#${headingId}`;
- return {children ?? packageName};
+ if (!needsManifest) {
+ return {children ?? packageName};
+ }
+
+ if (manifest === undefined) {
+ throw new Error(
+ `No API link manifest found for documentation version "${activeVersion.name}".`,
+ );
+ }
+
+ const defaultText = replacementPackage ?? packageName;
+ if (replacementPackage !== undefined && manifest[replacementPackage] !== undefined) {
+ warnOnce(
+ `PackageLink|replacementPackage|${activeVersion.name}|${replacementPackage}`,
+ `[PackageLink] Replacement package "${replacementPackage}" exists in API documentation version "${activeVersion.name}". Set package="${replacementPackage}" and remove the replacementPackage prop.`,
+ );
+ return (
+ {children ?? defaultText}
+ );
+ }
+
+ if (manifest[packageName] !== undefined) {
+ if (replacementPackage === undefined && newApi) {
+ warnOnce(
+ `PackageLink|newApi|${activeVersion.name}|${packageName}`,
+ `[PackageLink] Package "${packageName}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
+ );
+ }
+ return {children ?? defaultText};
+ }
+
+ if (newApi) {
+ return {children ?? defaultText};
+ }
+
+ return {children ?? defaultText};
}
/**
* {@link ApiLink} input props.
*/
-export interface ApiLinkProps {
+export interface ApiLinkProps<
+ TApiSelector extends string = string,
+ TReplacementApiSelector extends string = string,
+> {
/**
* Contents to display within the link.
* When omitted, the API declaration reference is displayed without selectors.
@@ -66,6 +126,21 @@ export interface ApiLinkProps {
*/
api: ApiDeclarationReference;
+ /**
+ * The declaration reference for the API after a rename.
+ *
+ * @remarks Remove this prop and update {@link ApiLinkProps.api} when the replacement API
+ * documentation is available.
+ */
+ replacementApi?: ApiDeclarationReference;
+
+ /**
+ * Permits the API to be absent from the published API documentation.
+ *
+ * @remarks Remove this prop when the API documentation is available.
+ */
+ newApi?: boolean;
+
/**
* Overrides the generated heading ID for the target API item.
*
@@ -79,48 +154,103 @@ export interface ApiLinkProps {
*
* @throws If the requested API cannot be uniquely resolved in the active documentation version.
*/
-export function ApiLink({
+export function ApiLink<
+ const TApiSelector extends string,
+ const TReplacementApiSelector extends string,
+>({
api,
package: packageName,
+ replacementApi,
+ newApi = false,
headingId,
children,
-}: ApiLinkProps): JSX.Element {
- const activePluginAndVersion = useActivePluginAndVersion();
- const manifests = usePluginData(apiLinkManifestPluginName, undefined, {
- failfast: true,
- }) as ApiLinkManifests;
- const activeVersion = activePluginAndVersion?.activeVersion;
- if (activeVersion === undefined) {
- throw new Error("ApiLink must be rendered within a versioned Docusaurus document.");
- }
-
- const manifest = manifests[activeVersion.name as SiteVersion];
+}: ApiLinkProps): JSX.Element {
+ const { activeVersion, manifest } = useApiLinkContext("ApiLink", true);
if (manifest === undefined) {
throw new Error(
`No API link manifest found for documentation version "${activeVersion.name}".`,
);
}
- const { target, defaultText } = resolveApiLinkTarget(manifest, packageName, api);
+ const replacementResult =
+ replacementApi === undefined
+ ? undefined
+ : tryResolveApiLinkTarget(manifest, packageName, replacementApi);
+ const result = tryResolveApiLinkTarget(manifest, packageName, api);
+ if (replacementResult?.found === true) {
+ warnOnce(
+ `ApiLink|replacementApi|${activeVersion.name}|${packageName}|${replacementApi}`,
+ `[ApiLink] Replacement API "${packageName}/${replacementApi}" exists in API documentation version "${activeVersion.name}". Set api="${replacementApi}" and remove the replacementApi prop.`,
+ );
+ return renderApiLink(
+ activeVersion.path,
+ replacementResult.target,
+ headingId,
+ children ?? replacementResult.defaultText,
+ );
+ }
+
+ const defaultText = replacementResult?.defaultText ?? result.defaultText;
+ if (!result.found) {
+ if (newApi) {
+ return {children ?? defaultText};
+ }
+ throw new Error(`No API documentation found for "${packageName}/${api}".`);
+ }
+
+ if (replacementApi === undefined && newApi) {
+ warnOnce(
+ `ApiLink|newApi|${activeVersion.name}|${packageName}|${api}`,
+ `[ApiLink] API "${packageName}/${api}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
+ );
+ }
+
+ return renderApiLink(activeVersion.path, result.target, headingId, children ?? defaultText);
+}
+
+function renderApiLink(
+ versionPath: string,
+ target: { readonly documentPath: string; readonly headingId?: string },
+ headingId: string | undefined,
+ children: ReactNode,
+): JSX.Element {
const targetHeadingId = headingId ?? target.headingId;
const headingPostfix = targetHeadingId === undefined ? "" : `#${targetHeadingId}`;
- return (
-
- {children ?? defaultText}
-
- );
+ return {children};
}
-/**
- * Gets the base URI for a link to API docs.
- * Accounts for versioning.
- */
-function useLinkPathBase(): string {
+function useApiLinkContext(
+ componentName: "ApiLink" | "PackageLink",
+ requireManifest: boolean,
+): {
+ readonly activeVersion: GlobalVersion;
+ readonly manifest: ApiLinkManifests[SiteVersion] | undefined;
+} {
const activeVersion = useActivePluginAndVersion()?.activeVersion;
+ const manifests = usePluginData(apiLinkManifestPluginName, undefined, {
+ failfast: requireManifest,
+ }) as ApiLinkManifests | undefined;
if (activeVersion === undefined) {
- throw new Error("PackageLink must be rendered within a versioned Docusaurus document.");
+ throw new Error(
+ `${componentName} must be rendered within a versioned Docusaurus document.`,
+ );
+ }
+
+ const manifest = manifests?.[activeVersion.name as SiteVersion];
+ if (requireManifest && manifest === undefined) {
+ throw new Error(
+ `No API link manifest found for documentation version "${activeVersion.name}".`,
+ );
+ }
+ return { activeVersion, manifest };
+}
+
+function warnOnce(key: string, message: string): void {
+ if (typeof window !== "undefined" || emittedTransitionWarnings.has(key)) {
+ return;
}
- return `${activeVersion.path}/api/`;
+ emittedTransitionWarnings.add(key);
+ console.warn(message);
}
/**
diff --git a/website/test/unit/shortLinks.test.ts b/website/test/unit/shortLinks.test.ts
index 4349b86f5f8..4c98077f298 100644
--- a/website/test/unit/shortLinks.test.ts
+++ b/website/test/unit/shortLinks.test.ts
@@ -5,7 +5,7 @@
import type { GlobalVersion } from "@docusaurus/plugin-content-docs/client";
import { ApiItemKind } from "@fluid-tools/api-markdown-documenter";
-import type { ReactElement, ReactNode } from "react";
+import { createElement, type ReactElement, type ReactNode } from "react";
import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
const { useActivePluginAndVersion, usePluginData } = vi.hoisted(() => ({
@@ -106,6 +106,8 @@ function renderApiLink(props: ApiLinkProps): { href: string; children: ReactNode
describe("PackageLink", () => {
afterEach(() => {
useActivePluginAndVersion.mockReset();
+ usePluginData.mockReset();
+ vi.restoreAllMocks();
});
it("uses the configured path for the active documentation version", () => {
@@ -121,12 +123,85 @@ describe("PackageLink", () => {
children: "example",
});
});
+
+ it("renders inline code when a new package is not documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+
+ const link = PackageLink({ package: "new-package", newApi: true });
+
+ expect({ type: link.type, children: link.props.children }).toEqual({
+ type: "code",
+ children: "new-package",
+ });
+ });
+
+ it("preserves rich children when a new package is not documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const children = createElement("strong", undefined, "New package");
+
+ const link = PackageLink({ package: "new-package", newApi: true, children });
+
+ expect(link.type).toBe("code");
+ expect(link.props.children).toBe(children);
+ });
+
+ it("links and warns when a new package is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = PackageLink({ package: "example", newApi: true });
+ PackageLink({ package: "example", newApi: true });
+
+ expect({ type: link.type, href: link.props.href, children: link.props.children }).toEqual({
+ type: "a",
+ href: "/docs/api/example",
+ children: "example",
+ });
+ expect(warn).toHaveBeenCalledWith(
+ '[PackageLink] Package "example" exists in API documentation version "current". Remove the newApi prop.',
+ );
+ expect(warn).toHaveBeenCalledTimes(1);
+ });
+
+ it("falls back to the original package until its replacement is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = PackageLink({ package: "example", replacementPackage: "replacement" });
+
+ expect({ href: link.props.href, children: link.props.children }).toEqual({
+ href: "/docs/api/example",
+ children: "replacement",
+ });
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it("uses and warns about a documented replacement package", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = PackageLink({ package: "old-package", replacementPackage: "example" });
+
+ expect({ href: link.props.href, children: link.props.children }).toEqual({
+ href: "/docs/api/example",
+ children: "example",
+ });
+ expect(warn).toHaveBeenCalledWith(
+ '[PackageLink] Replacement package "example" exists in API documentation version "current". Set package="example" and remove the replacementPackage prop.',
+ );
+ });
});
describe("ApiLink", () => {
afterEach(() => {
useActivePluginAndVersion.mockReset();
usePluginData.mockReset();
+ vi.restoreAllMocks();
});
it("uses the active version manifest and versioned path", () => {
@@ -190,6 +265,102 @@ describe("ApiLink", () => {
});
});
+ it("renders inline code with rich children when a new API is not documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const children = createElement("strong", undefined, "New API");
+
+ const link = ApiLink({ package: "example", api: "Missing", newApi: true, children });
+
+ expect(link.type).toBe("code");
+ expect(link.props.children).toBe(children);
+ });
+
+ it("links and warns when a new API is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = renderApiLink({ package: "example", api: "(Widget:class)", newApi: true });
+
+ expect(link).toEqual({
+ href: "/docs/api/example/widget-class",
+ children: "Widget",
+ });
+ expect(warn).toHaveBeenCalledWith(
+ '[ApiLink] API "example/(Widget:class)" exists in API documentation version "current". Remove the newApi prop.',
+ );
+ });
+
+ it("falls back to the original API until its replacement is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = renderApiLink({
+ package: "example",
+ api: "(Widget:class)",
+ replacementApi: "Replacement",
+ });
+
+ expect(link).toEqual({
+ href: "/docs/api/example/widget-class",
+ children: "Replacement",
+ });
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it("uses and warns about a documented replacement API", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ const link = renderApiLink({
+ package: "example",
+ api: "Missing",
+ replacementApi: "(Widget:class)",
+ });
+
+ expect(link).toEqual({
+ href: "/docs/api/example/widget-class",
+ children: "Widget",
+ });
+ expect(warn).toHaveBeenCalledWith(
+ '[ApiLink] Replacement API "example/(Widget:class)" exists in API documentation version "current". Set api="(Widget:class)" and remove the replacementApi prop.',
+ );
+ });
+
+ it("does not let newApi hide an invalid replacement reference", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+
+ expect(() =>
+ ApiLink({
+ package: "example",
+ api: "Missing",
+ replacementApi: "(Widget:static)" as string,
+ newApi: true,
+ }),
+ ).toThrowError(
+ 'Unsupported selector "static" in API declaration reference "(Widget:static)".',
+ );
+ });
+
+ it("does not let a replacement hide an invalid original reference", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+
+ expect(() =>
+ ApiLink({
+ package: "example",
+ api: "(Widget:static)" as string,
+ replacementApi: "(Widget:class)",
+ }),
+ ).toThrowError(
+ 'Unsupported selector "static" in API declaration reference "(Widget:static)".',
+ );
+ });
+
it("throws when rendered outside a versioned Docusaurus document", () => {
useActivePluginAndVersion.mockReturnValue(undefined);
useMockApiLinkManifests();
@@ -278,5 +449,8 @@ describe("ApiDeclarationReference", () => {
expectTypeOf>().toEqualTypeOf();
expectTypeOf>().toEqualTypeOf();
expectTypeOf>().toEqualTypeOf();
+ expectTypeOf<
+ ApiLinkProps<"Widget", "(Replacement:class)">["replacementApi"]
+ >().toEqualTypeOf<"(Replacement:class)" | undefined>();
});
});
From b0fb2dacc55ce0bf7a583beb19db77731097522c Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:32:14 +0000
Subject: [PATCH 03/11] refactor(website): Simpler rename pattern
---
website/README.md | 31 ++++--
website/STAGED_API_LINKS_DESIGN.md | 148 +++++++++++++++-----------
website/src/components/shortLinks.tsx | 124 ++++++++++++---------
website/test/unit/shortLinks.test.ts | 70 +++++++++---
4 files changed, 231 insertions(+), 142 deletions(-)
diff --git a/website/README.md b/website/README.md
index 208da00ce27..e1bda63ffda 100644
--- a/website/README.md
+++ b/website/README.md
@@ -190,26 +190,35 @@ 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)`.
-Use `replacementApi` when an API is renamed.
-Keep the old reference in `api` and put the new reference in `replacementApi`:
+For an API rename, set `api` to an object with `previous` and `new` declaration references:
```mdx
-
+
```
-The component tries `replacementApi` first.
-It uses `api` while the published model contains only the old API.
-The visible default text uses the replacement name in both states.
-When the replacement exists, the component writes a build warning.
-Copy the `replacementApi` value to `api`, and then remove `replacementApi`.
+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.
+When `new` exists, the component writes a build warning.
+Replace the object with the new declaration reference at that time:
-Use `replacementPackage` in the same way for a package rename:
+```mdx
+
+```
+
+For a package rename, set `package` to an object with `previous` and `new` names:
```mdx
-
+
```
-When the replacement package exists, copy its name to `package`, and then remove `replacementPackage`.
+The component uses `previous` while the published model contains only the old package.
+When the `new` package exists, the component writes a build warning.
+Replace the object with the new string value at that time:
+
+```mdx
+
+```
All transition modes permit explicit rich child content.
The component preserves the child content when it renders a link or inline code:
diff --git a/website/STAGED_API_LINKS_DESIGN.md b/website/STAGED_API_LINKS_DESIGN.md
index c6327122c2b..f0a8066f10f 100644
--- a/website/STAGED_API_LINKS_DESIGN.md
+++ b/website/STAGED_API_LINKS_DESIGN.md
@@ -5,8 +5,8 @@
Add two transition mechanisms to `PackageLink` and `ApiLink`:
- A `newApi` prop permits a target to be absent from the active API documentation. The component renders inline code when the target is absent. When the target exists, the component renders a link and logs a warning that tells the author to remove `newApi`.
-- A `replacementApi` prop gives `ApiLink` a preferred API reference. The component tries this reference before the reference in `api`. It uses `api` as a fallback while published API documentation still contains the old name. When `replacementApi` resolves, the component logs a warning that tells the author to make the new reference permanent.
-- A `replacementPackage` prop gives `PackageLink` the same rename behavior for package names.
+- An object value for `ApiLink.api` gives `ApiLink` a preferred API reference in `new` and a fallback reference in `previous`. When `new` resolves, the component logs a warning that tells the author to make the new reference permanent.
+- An object value for `PackageLink.package` gives `PackageLink` the same rename behavior for package names. The object contains `previous` and `new` names.
These mechanisms let a website change merge with its related API change. The website can then work before and after release artifacts contain the API change.
@@ -43,13 +43,17 @@ The design does not do these tasks:
### `PackageLink`
-Add optional `newApi` and `replacementPackage` props:
+Update `package` to accept a string or a staged rename object. Add the optional `newApi` prop:
```tsx
+export interface PackageLinkRename {
+ previous: string;
+ new: string;
+}
+
export interface PackageLinkProps {
children?: ReactNode;
- package: string;
- replacementPackage?: string;
+ package: string | PackageLinkRename;
headingId?: string;
newApi?: boolean;
}
@@ -66,24 +70,31 @@ The package name remains unscoped. This rule is the same as the current rule.
An MDX file can prepare for a package rename:
```mdx
-
+
```
-`replacementPackage` contains the preferred unscoped package name. The `package` prop remains the fallback name. When the transition is complete, the author copies the `replacementPackage` value to `package` and removes `replacementPackage`.
+`previous` contains the fallback unscoped package name. `new` contains the preferred unscoped package name. When the transition is complete, the author replaces the object with the `new` string value.
### `ApiLink`
-Add optional `newApi` and `replacementApi` props:
+Update `api` to accept a declaration reference or a staged rename object. Add the optional `newApi` prop:
```tsx
+export interface ApiLinkRename<
+ TPreviousApiSelector extends string = string,
+ TNewApiSelector extends string = string,
+> {
+ previous: ApiDeclarationReference;
+ new: ApiDeclarationReference;
+}
+
export interface ApiLinkProps<
TApiSelector extends string = string,
- TReplacementApiSelector extends string = string,
+ TNewApiSelector extends string = string,
> {
children?: ReactNode;
package: string;
- api: ApiDeclarationReference;
- replacementApi?: ApiDeclarationReference;
+ api: ApiDeclarationReference | ApiLinkRename;
newApi?: boolean;
headingId?: string;
}
@@ -100,12 +111,12 @@ Because `newApi` has the `boolean` type, MDX and JSX permit the shorthand `newAp
An MDX file can prepare for a rename:
```mdx
-
+
```
-`replacementApi` uses the same declaration-reference grammar as `api`. An author specifies an API item kind with a TSDoc selector. For example, `(NewApi:class)` selects a class and `(NewApi:interface).method` selects a method on an interface. A separate API-kind prop is not necessary.
+Both object properties use the same declaration-reference grammar as a string `api` value. An author specifies an API item kind with a TSDoc selector. For example, `(NewApi:class)` selects a class and `(NewApi:interface).method` selects a method on an interface. A separate API-kind prop is not necessary.
-The `api` prop remains the fallback reference. When the transition is complete, the author copies the `replacementApi` value to `api` and removes `replacementApi`.
+`previous` is the fallback reference. `new` is the preferred reference. When the transition is complete, the author replaces the object with the `new` string value.
## Resolution behavior
@@ -115,9 +126,9 @@ All checks use the manifest for the active Docusaurus documentation version. A t
A normal `PackageLink` keeps its current behavior. It creates the package URL without a manifest lookup.
-A `PackageLink` with `replacementPackage` checks the replacement package first. If the replacement package does not exist, it uses `package`. When the replacement package exists, the component links to it and logs a warning to make the replacement permanent.
+A `PackageLink` with an object `package` value checks the `new` package first. If the new package does not exist, it uses `previous`. When the new package exists, the component links to it and logs a warning to make the new name permanent.
-The following table applies when `replacementPackage` is not present:
+The following table applies when `package` is a string:
| `newApi` | Package exists | Result |
| -------- | -------------- | ----------------------------------------------------------- |
@@ -125,33 +136,42 @@ The following table applies when `replacementPackage` is not present:
| `true` | No | Render the content as inline code. Do not throw. |
| `true` | Yes | Render the link. Log a warning to remove `newApi`. |
-The following table applies when `replacementPackage` is present:
+The following table applies when `package` is an object:
-| Replacement package exists | Original package exists | `newApi` | Result |
-| -------------------------- | ----------------------- | ------------ | ----------------------------------------------------------------------------------------------- |
-| Yes | Either state | Either state | Link to `replacementPackage`. Log a warning to replace `package` and remove the temporary prop. |
-| No | Yes | Either state | Link to `package`. Do not log a transition warning. |
-| No | No | `true` | Render inline code. Do not throw. |
-| No | No | Not set | Render the current original-package link. Existing broken-link checks apply. |
+| Replacement package exists | Original package exists | `newApi` | Result |
+| -------------------------- | ----------------------- | ------------ | ----------------------------------------------------------------------------- |
+| Yes | Either state | Either state | Link to `new`. Log a warning to replace the object with the new package name. |
+| No | Yes | Either state | Link to `previous`. Do not log a transition warning. |
+| No | No | `true` | Render inline code. Do not throw. |
+| No | No | Not set | Render the current original-package link. Existing broken-link checks apply. |
-If `replacementPackage` and `newApi` are both present, replacement resolution runs first. `newApi` controls only the final state in which neither package exists.
+If the object form and `newApi` are both present, rename resolution runs first. `newApi` controls only the final state in which neither package exists.
The manifest identifies package existence. It does not contain package-heading metadata. Therefore, `headingId` continues to use the normal Docusaurus anchor check.
### `ApiLink` behavior
-`ApiLink` resolves `replacementApi` first when that prop is present. If the preferred reference is not found, it resolves `api`.
+`ApiLink` resolves `new` first when `api` is an object. If the preferred reference is not found, it resolves `previous`.
+
+The following table applies when `api` is a string:
+
+| `api` target | `newApi` | Result |
+| -------------- | -------- | ------------------------------------------------ |
+| Exists | `true` | Link to `api`. Log a warning to remove `newApi`. |
+| Exists | Not set | Render the current link. |
+| Does not exist | `true` | Render inline code. Do not throw. |
+| Does not exist | Not set | Throw the current not-found error. |
+
+The following table applies when `api` is an object:
-| `replacementApi` target | `api` target | `newApi` | Result |
-| --------------------------------- | -------------- | ------------ | ------------------------------------------------------------------------------------- |
-| Exists | Either state | Either state | Link to `replacementApi`. Log a warning to replace `api` and remove `replacementApi`. |
-| Does not exist | Exists | Either state | Link to `api`. Do not log a transition warning. |
-| Not supplied | Exists | `true` | Link to `api`. Log a warning to remove `newApi`. |
-| Not supplied | Exists | Not set | Render the current link. |
-| Does not exist or is not supplied | Does not exist | `true` | Render inline code. Do not throw. |
-| Does not exist or is not supplied | Does not exist | Not set | Throw the current not-found error. |
+| `new` target | `previous` target | `newApi` | Result |
+| -------------- | ----------------- | ------------ | ------------------------------------------------------------------------------ |
+| Exists | Either state | Either state | Link to `new`. Log a warning to replace the object with the new API reference. |
+| Does not exist | Exists | Either state | Link to `previous`. Do not log a transition warning. |
+| Does not exist | Does not exist | `true` | Render inline code. Do not throw. |
+| Does not exist | Does not exist | Not set | Throw the current not-found error for `previous`. |
-If both `replacementApi` and `newApi` are present, replacement resolution runs first. `newApi` only controls the final state in which neither reference exists. This combination supports a new API that changes name before its first API documentation artifact is published.
+If the object form and `newApi` are both present, rename resolution runs first. `newApi` only controls the final state in which neither reference exists. This combination supports a new API that changes name before its first API documentation artifact is published.
### Errors that remain strict
@@ -175,21 +195,21 @@ Each component preserves explicit `children`, including rich React content:
{children ?? defaultText}
```
-For `PackageLink` without `replacementPackage`, `defaultText` is the package name.
+For `PackageLink` with a string `package` value, `defaultText` is the package name.
-For `PackageLink` with `replacementPackage`, `defaultText` is always the replacement package name. This rule applies when the component links to the original package.
+For `PackageLink` with an object `package` value, `defaultText` is always `new`. This rule applies when the component links to `previous`.
-For `ApiLink` without `replacementApi`, `defaultText` is the selector-free dotted path from `api`. This behavior matches the current link text.
+For `ApiLink` with a string `api` value, `defaultText` is the selector-free dotted path from `api`. This behavior matches the current link text.
-For `ApiLink` with `replacementApi`, `defaultText` is always the selector-free dotted path from `replacementApi`. This rule applies when the component links to the old target. It lets the visible documentation use the new API name before the new API documentation exists.
+For `ApiLink` with an object `api` value, `defaultText` is the selector-free dotted path for the target that resolves. It uses `previous` while the old target resolves and `new` after the new target resolves. If neither target resolves and `newApi` is present, the inline-code fallback uses `new`.
For example, this source:
```mdx
-
+
```
-renders visible text `NewApi` in both transition states. Before publication, the link points to `OldApi`. After publication, the link points to `NewApi`.
+renders visible text `OldApi` and links to `OldApi` before publication. After publication, it renders visible text `NewApi` and links to `NewApi`.
All transition states permit rich child content. The component puts the same child tree in the link or the inline-code fallback. Authors can use inline elements such as emphasis and API display formatting. Authors should not add an outer inline-code element because the fallback already creates one.
@@ -257,11 +277,11 @@ Warnings should use stable and actionable text. Proposed messages are:
```
```text
-[ApiLink] Replacement API "fluid-framework/NewApi" exists in API documentation version "current". Set api="NewApi" and remove the replacementApi prop.
+[ApiLink] New API name "fluid-framework/NewApi" exists in API documentation version "current". Set api="NewApi".
```
```text
-[PackageLink] Replacement package "new-package" exists in API documentation version "current". Set package="new-package" and remove the replacementPackage prop.
+[PackageLink] New package name "new-package" exists in API documentation version "current". Set package="new-package".
```
The actual message must preserve selector syntax when it tells the author which value to put in `api`.
@@ -304,7 +324,7 @@ Update `website/src/components/shortLinks.tsx`:
1. Add a helper that gets the active version and its manifest.
2. Use this helper in `ApiLink`.
-3. Use this helper in `PackageLink` when `newApi` is true or `replacementPackage` is present.
+3. Use this helper in `PackageLink` when `newApi` is true or `package` is an object.
4. Preserve the current normal `PackageLink` path construction.
The helper must keep the current errors for a missing version context and a missing manifest. Error text can name the calling component.
@@ -317,7 +337,7 @@ Update `PackageLink` and `ApiLink`:
2. Implement the resolution tables in this document.
3. Render `` for the permitted final not-found state.
4. Keep explicit children unchanged.
-5. Use the preferred replacement reference or package name for default display text.
+5. Use the preferred new reference or package name for default display text.
6. Preserve rich child content in link and inline-code output.
### 4. Add warning support
@@ -328,7 +348,7 @@ Do not add warning data to the generated manifest. The warning depends on compon
### 5. Add documentation examples
-Add contributor guidance for these props after implementation. State that both props are temporary. Include the required cleanup operation for each warning.
+Add contributor guidance for these transition values after implementation. State that `newApi` and the rename object forms are temporary. Include the required cleanup operation for each warning.
Do not add a new-API example to published product documentation only to test the component. Use unit tests for transition states.
@@ -341,10 +361,10 @@ Extend `website/test/unit/shortLinks.test.ts` and the resolver unit tests.
Verify these cases:
- `newApi` is accepted by both components with JSX boolean shorthand.
-- `replacementApi` accepts a valid dotted reference.
-- `replacementApi` accepts a kind selector.
-- `replacementApi` rejects an invalid literal reference.
-- `replacementPackage` accepts a string.
+- `api` accepts an object with valid `previous` and `new` declaration references.
+- The object properties accept kind selectors.
+- The object properties reject invalid literal references.
+- `package` accepts a string or an object with string `previous` and `new` properties.
- Existing `api` type checks continue to work.
### `PackageLink` tests
@@ -357,9 +377,9 @@ Verify these cases:
- An existing package renders a link when `newApi` is present.
- An existing package logs the cleanup warning when `newApi` is present.
- The warning includes the active version.
-- A missing `replacementPackage` target falls back to `package` without a warning.
-- An existing `replacementPackage` target wins and logs a cleanup warning.
-- Default text uses `replacementPackage` before and after the target exists.
+- A missing `new` package falls back to `previous` without a warning.
+- An existing `new` package wins and logs a cleanup warning.
+- Default text uses `new` before and after the package target exists.
- Explicit rich children remain unchanged before and after the target exists.
- If both package names are absent, `newApi` controls code output versus the current original-package link.
@@ -370,9 +390,9 @@ Verify these cases:
- A missing API renders `` when `newApi` is present.
- A missing API preserves explicit children when `newApi` is present.
- An existing API renders a link and logs the cleanup warning when `newApi` is present.
-- A missing `replacementApi` target falls back to `api` without a warning.
-- An existing `replacementApi` target wins and logs a cleanup warning.
-- Default text uses `replacementApi` before and after the target exists.
+- A missing `new` target falls back to `previous` without a warning.
+- An existing `new` target wins and logs a cleanup warning.
+- Default text uses the API reference that resolves.
- Explicit rich children remain unchanged before and after the target exists.
- If both references are absent, `newApi` controls code output versus an error.
- A malformed preferred reference throws and does not fall back.
@@ -441,23 +461,23 @@ For a new API or package:
For a renamed API:
-1. Keep the old reference in `api`.
-2. Put the new qualified reference in `replacementApi`.
-3. The website displays the new name and links to the old API documentation before publication.
+1. Put the old qualified reference in `api.previous`.
+2. Put the new qualified reference in `api.new`.
+3. The website displays the old name and links to the old API documentation before publication when child content is omitted.
4. The website links to the new API documentation and logs a warning after publication.
-5. Copy the `replacementApi` value to `api` and remove `replacementApi`.
+5. Replace the `api` object with the value from `api.new`.
For a renamed package:
-1. Keep the old package name in `package`.
-2. Put the new unscoped package name in `replacementPackage`.
+1. Put the old unscoped package name in `package.previous`.
+2. Put the new unscoped package name in `package.new`.
3. The website displays the new package name and links to the old package documentation before publication.
4. The website links to the new package documentation and logs a warning after publication.
-5. Copy the `replacementPackage` value to `package` and remove `replacementPackage`.
+5. Replace the `package` object with the value from `package.new`.
## Resolved design decisions
-1. Use `replacementApi` for API rename transitions.
+1. Use the object form of `ApiLink.api` for API rename transitions.
2. Use best-effort build warnings. Do not add stricter cleanup enforcement now.
-3. Support package rename transitions with `replacementPackage` in the first implementation.
+3. Support package rename transitions with the object form of `PackageLink.package` in the first implementation.
4. Permit rich child content in all link and fallback states.
diff --git a/website/src/components/shortLinks.tsx b/website/src/components/shortLinks.tsx
index b6f354ede49..e7083335f77 100644
--- a/website/src/components/shortLinks.tsx
+++ b/website/src/components/shortLinks.tsx
@@ -16,28 +16,35 @@ import type { SiteVersion } from "../utilityTypes";
const emittedTransitionWarnings = new Set();
+/**
+ * The package names used while API documentation transitions through a package rename.
+ */
+export interface PackageLinkRename {
+ /**
+ * The package name in the current published API documentation.
+ */
+ previous: string;
+
+ /**
+ * The new package name that will replace {@link PackageLinkRename.previous}.
+ */
+ new: string;
+}
+
/**
* {@link PackageLink} input props.
*/
export interface PackageLinkProps {
/**
* Contents to display within the link.
- * @defaultValue {@link PackageLinkProps.package}
+ * When omitted during a package rename, the new package name is displayed.
*/
children?: ReactNode;
/**
- * The unscoped name of the package whose API documentation is linked.
- */
- package: string;
-
- /**
- * The new unscoped package name after a package rename.
- *
- * @remarks Remove this prop and update {@link PackageLinkProps.package} when the new package API
- * documentation is available.
+ * The unscoped package name, or the previous and new names for a staged package rename.
*/
- replacementPackage?: string;
+ package: string | PackageLinkRename;
headingId?: string;
@@ -54,12 +61,17 @@ export interface PackageLinkProps {
*/
export function PackageLink({
headingId,
- package: packageName,
- replacementPackage,
+ package: packageNameOrRename,
children,
newApi = false,
}: PackageLinkProps): JSX.Element {
- const needsManifest = newApi || replacementPackage !== undefined;
+ const rename = typeof packageNameOrRename === "string" ? undefined : packageNameOrRename;
+ const packageName =
+ typeof packageNameOrRename === "string"
+ ? packageNameOrRename
+ : packageNameOrRename.previous;
+ const newPackageName = rename?.new;
+ const needsManifest = newApi || rename !== undefined;
const { activeVersion, manifest } = useApiLinkContext("PackageLink", needsManifest);
const root = `${activeVersion.path}/api/`;
const headingPostfix = headingId === undefined ? "" : `#${headingId}`;
@@ -73,19 +85,17 @@ export function PackageLink({
);
}
- const defaultText = replacementPackage ?? packageName;
- if (replacementPackage !== undefined && manifest[replacementPackage] !== undefined) {
+ const defaultText = newPackageName ?? packageName;
+ if (newPackageName !== undefined && manifest[newPackageName] !== undefined) {
warnOnce(
- `PackageLink|replacementPackage|${activeVersion.name}|${replacementPackage}`,
- `[PackageLink] Replacement package "${replacementPackage}" exists in API documentation version "${activeVersion.name}". Set package="${replacementPackage}" and remove the replacementPackage prop.`,
- );
- return (
- {children ?? defaultText}
+ `PackageLink|rename|${activeVersion.name}|${newPackageName}`,
+ `[PackageLink] New package name "${newPackageName}" exists in API documentation version "${activeVersion.name}". Set package="${newPackageName}".`,
);
+ return {children ?? defaultText};
}
if (manifest[packageName] !== undefined) {
- if (replacementPackage === undefined && newApi) {
+ if (rename === undefined && newApi) {
warnOnce(
`PackageLink|newApi|${activeVersion.name}|${packageName}`,
`[PackageLink] Package "${packageName}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
@@ -101,18 +111,34 @@ export function PackageLink({
return {children ?? defaultText};
}
+/**
+ * The declaration references used while API documentation transitions through an API rename.
+ */
+export interface ApiLinkRename<
+ TPreviousApiSelector extends string = string,
+ TNewApiSelector extends string = string,
+> {
+ /**
+ * The API declaration reference in the current published API documentation.
+ */
+ previous: ApiDeclarationReference;
+
+ /**
+ * The new API declaration reference that will replace {@link ApiLinkRename.previous}.
+ */
+ new: ApiDeclarationReference;
+}
+
/**
* {@link ApiLink} input props.
*/
export interface ApiLinkProps<
TApiSelector extends string = string,
- TReplacementApiSelector extends string = string,
+ TNewApiSelector extends string = string,
> {
/**
* Contents to display within the link.
- * When omitted, the API declaration reference is displayed without selectors.
- *
- * @defaultValue {@link ApiLinkProps.api}
+ * When omitted, the resolved API declaration reference is displayed without selectors.
*/
children?: ReactNode;
@@ -122,17 +148,10 @@ export interface ApiLinkProps<
package: string;
/**
- * A TSDoc-style declaration reference identifying the API item within the package.
- */
- api: ApiDeclarationReference;
-
- /**
- * The declaration reference for the API after a rename.
- *
- * @remarks Remove this prop and update {@link ApiLinkProps.api} when the replacement API
- * documentation is available.
+ * A TSDoc-style declaration reference, or the previous and new references for a staged API
+ * rename.
*/
- replacementApi?: ApiDeclarationReference;
+ api: ApiDeclarationReference | ApiLinkRename;
/**
* Permits the API to be absent from the published API documentation.
@@ -154,17 +173,13 @@ export interface ApiLinkProps<
*
* @throws If the requested API cannot be uniquely resolved in the active documentation version.
*/
-export function ApiLink<
- const TApiSelector extends string,
- const TReplacementApiSelector extends string,
->({
- api,
+export function ApiLink({
+ api: apiOrRename,
package: packageName,
- replacementApi,
newApi = false,
headingId,
children,
-}: ApiLinkProps): JSX.Element {
+}: ApiLinkProps): JSX.Element {
const { activeVersion, manifest } = useApiLinkContext("ApiLink", true);
if (manifest === undefined) {
throw new Error(
@@ -172,15 +187,18 @@ export function ApiLink<
);
}
+ const rename = typeof apiOrRename === "string" ? undefined : apiOrRename;
+ const api = typeof apiOrRename === "string" ? apiOrRename : apiOrRename.previous;
+ const newApiReference = rename?.new;
const replacementResult =
- replacementApi === undefined
+ newApiReference === undefined
? undefined
- : tryResolveApiLinkTarget(manifest, packageName, replacementApi);
+ : tryResolveApiLinkTarget(manifest, packageName, newApiReference);
const result = tryResolveApiLinkTarget(manifest, packageName, api);
if (replacementResult?.found === true) {
warnOnce(
- `ApiLink|replacementApi|${activeVersion.name}|${packageName}|${replacementApi}`,
- `[ApiLink] Replacement API "${packageName}/${replacementApi}" exists in API documentation version "${activeVersion.name}". Set api="${replacementApi}" and remove the replacementApi prop.`,
+ `ApiLink|rename|${activeVersion.name}|${packageName}|${newApiReference}`,
+ `[ApiLink] New API name "${packageName}/${newApiReference}" exists in API documentation version "${activeVersion.name}". Set api="${newApiReference}".`,
);
return renderApiLink(
activeVersion.path,
@@ -190,22 +208,26 @@ export function ApiLink<
);
}
- const defaultText = replacementResult?.defaultText ?? result.defaultText;
if (!result.found) {
if (newApi) {
- return {children ?? defaultText};
+ return {children ?? replacementResult?.defaultText ?? result.defaultText};
}
throw new Error(`No API documentation found for "${packageName}/${api}".`);
}
- if (replacementApi === undefined && newApi) {
+ if (rename === undefined && newApi) {
warnOnce(
`ApiLink|newApi|${activeVersion.name}|${packageName}|${api}`,
`[ApiLink] API "${packageName}/${api}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
);
}
- return renderApiLink(activeVersion.path, result.target, headingId, children ?? defaultText);
+ return renderApiLink(
+ activeVersion.path,
+ result.target,
+ headingId,
+ children ?? result.defaultText,
+ );
}
function renderApiLink(
diff --git a/website/test/unit/shortLinks.test.ts b/website/test/unit/shortLinks.test.ts
index 4c98077f298..c8a075463eb 100644
--- a/website/test/unit/shortLinks.test.ts
+++ b/website/test/unit/shortLinks.test.ts
@@ -171,7 +171,9 @@ describe("PackageLink", () => {
useMockApiLinkManifests();
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
- const link = PackageLink({ package: "example", replacementPackage: "replacement" });
+ const link = PackageLink({
+ package: { previous: "example", new: "replacement" },
+ });
expect({ href: link.props.href, children: link.props.children }).toEqual({
href: "/docs/api/example",
@@ -185,14 +187,16 @@ describe("PackageLink", () => {
useMockApiLinkManifests();
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
- const link = PackageLink({ package: "old-package", replacementPackage: "example" });
+ const link = PackageLink({
+ package: { previous: "old-package", new: "example" },
+ });
expect({ href: link.props.href, children: link.props.children }).toEqual({
href: "/docs/api/example",
children: "example",
});
expect(warn).toHaveBeenCalledWith(
- '[PackageLink] Replacement package "example" exists in API documentation version "current". Set package="example" and remove the replacementPackage prop.',
+ '[PackageLink] New package name "example" exists in API documentation version "current". Set package="example".',
);
});
});
@@ -299,17 +303,47 @@ describe("ApiLink", () => {
const link = renderApiLink({
package: "example",
- api: "(Widget:class)",
- replacementApi: "Replacement",
+ api: { previous: "(Widget:class)", new: "Replacement" },
});
expect(link).toEqual({
href: "/docs/api/example/widget-class",
- children: "Replacement",
+ children: "Widget",
});
expect(warn).not.toHaveBeenCalled();
});
+ it("preserves rich children while an API rename is staged", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const children = createElement("strong", undefined, "Renamed API");
+
+ const link = ApiLink({
+ package: "example",
+ api: { previous: "(Widget:class)", new: "Replacement" },
+ children,
+ });
+
+ expect(link.type).toBe("a");
+ expect(link.props.children).toBe(children);
+ });
+
+ it("renders inline code when neither API rename target is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+
+ const link = ApiLink({
+ package: "example",
+ api: { previous: "OldMissing", new: "NewMissing" },
+ newApi: true,
+ });
+
+ expect({ type: link.type, children: link.props.children }).toEqual({
+ type: "code",
+ children: "NewMissing",
+ });
+ });
+
it("uses and warns about a documented replacement API", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
@@ -317,8 +351,7 @@ describe("ApiLink", () => {
const link = renderApiLink({
package: "example",
- api: "Missing",
- replacementApi: "(Widget:class)",
+ api: { previous: "Missing", new: "(Widget:class)" },
});
expect(link).toEqual({
@@ -326,7 +359,7 @@ describe("ApiLink", () => {
children: "Widget",
});
expect(warn).toHaveBeenCalledWith(
- '[ApiLink] Replacement API "example/(Widget:class)" exists in API documentation version "current". Set api="(Widget:class)" and remove the replacementApi prop.',
+ '[ApiLink] New API name "example/(Widget:class)" exists in API documentation version "current". Set api="(Widget:class)".',
);
});
@@ -337,8 +370,7 @@ describe("ApiLink", () => {
expect(() =>
ApiLink({
package: "example",
- api: "Missing",
- replacementApi: "(Widget:static)" as string,
+ api: { previous: "Missing", new: "(Widget:static)" as string },
newApi: true,
}),
).toThrowError(
@@ -353,8 +385,10 @@ describe("ApiLink", () => {
expect(() =>
ApiLink({
package: "example",
- api: "(Widget:static)" as string,
- replacementApi: "(Widget:class)",
+ api: {
+ previous: "(Widget:static)" as string,
+ new: "(Widget:class)",
+ },
}),
).toThrowError(
'Unsupported selector "static" in API declaration reference "(Widget:static)".',
@@ -449,8 +483,12 @@ describe("ApiDeclarationReference", () => {
expectTypeOf>().toEqualTypeOf();
expectTypeOf>().toEqualTypeOf();
expectTypeOf>().toEqualTypeOf();
- expectTypeOf<
- ApiLinkProps<"Widget", "(Replacement:class)">["replacementApi"]
- >().toEqualTypeOf<"(Replacement:class)" | undefined>();
+ expectTypeOf["api"]>().toEqualTypeOf<
+ | "Widget"
+ | {
+ previous: "Widget";
+ new: "(Replacement:class)";
+ }
+ >();
});
});
From 5ff98d01086a4fec810eaa4fbb7da98441ae0cfa Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:34:48 +0000
Subject: [PATCH 04/11] refactor(website): Remove deprecated `headingId`
support
---
website/STAGED_API_LINKS_DESIGN.md | 13 -----------
website/src/components/shortLinks.tsx | 32 +++++----------------------
website/test/unit/shortLinks.test.ts | 16 --------------
3 files changed, 6 insertions(+), 55 deletions(-)
diff --git a/website/STAGED_API_LINKS_DESIGN.md b/website/STAGED_API_LINKS_DESIGN.md
index f0a8066f10f..3641046d477 100644
--- a/website/STAGED_API_LINKS_DESIGN.md
+++ b/website/STAGED_API_LINKS_DESIGN.md
@@ -36,7 +36,6 @@ The design does not do these tasks:
- It does not permit arbitrary broken links.
- It does not add a fallback URL supplied by an author.
- It does not find renamed APIs automatically.
-- It does not validate an arbitrary `headingId` in `PackageLink`.
- It does not make transition warnings fail the build.
## Proposed component API
@@ -54,7 +53,6 @@ export interface PackageLinkRename {
export interface PackageLinkProps {
children?: ReactNode;
package: string | PackageLinkRename;
- headingId?: string;
newApi?: boolean;
}
```
@@ -96,7 +94,6 @@ export interface ApiLinkProps<
package: string;
api: ApiDeclarationReference | ApiLinkRename;
newApi?: boolean;
- headingId?: string;
}
```
@@ -147,8 +144,6 @@ The following table applies when `package` is an object:
If the object form and `newApi` are both present, rename resolution runs first. `newApi` controls only the final state in which neither package exists.
-The manifest identifies package existence. It does not contain package-heading metadata. Therefore, `headingId` continues to use the normal Docusaurus anchor check.
-
### `ApiLink` behavior
`ApiLink` resolves `new` first when `api` is an object. If the preferred reference is not found, it resolves `previous`.
@@ -298,14 +293,6 @@ Only emit these warnings when `typeof window === "undefined"`. This rule prevent
Warnings are best-effort build messages. They must not fail the build. A later change can add a stricter cleanup mechanism if the team needs one.
-## `headingId` behavior
-
-`ApiLink.headingId` is deprecated. The new resolution order does not change that status.
-
-When `headingId` is present, it overrides the generated heading ID for the target that wins API resolution. Authors should not use it for an API replacement transition because one override might not be correct for both targets. Qualified declaration references remain the supported solution.
-
-`PackageLink.headingId` keeps its current behavior. One heading ID must work with both package targets during a package replacement transition.
-
## Implementation plan
### 1. Refactor API reference resolution
diff --git a/website/src/components/shortLinks.tsx b/website/src/components/shortLinks.tsx
index e7083335f77..5916bee2f97 100644
--- a/website/src/components/shortLinks.tsx
+++ b/website/src/components/shortLinks.tsx
@@ -46,8 +46,6 @@ export interface PackageLinkProps {
*/
package: string | PackageLinkRename;
- headingId?: string;
-
/**
* Permits the package to be absent from the published API documentation.
*
@@ -60,7 +58,6 @@ export interface PackageLinkProps {
* A convenient mechanism for linking to a package's API documentation.
*/
export function PackageLink({
- headingId,
package: packageNameOrRename,
children,
newApi = false,
@@ -74,9 +71,8 @@ export function PackageLink({
const needsManifest = newApi || rename !== undefined;
const { activeVersion, manifest } = useApiLinkContext("PackageLink", needsManifest);
const root = `${activeVersion.path}/api/`;
- const headingPostfix = headingId === undefined ? "" : `#${headingId}`;
if (!needsManifest) {
- return {children ?? packageName};
+ return {children ?? packageName};
}
if (manifest === undefined) {
@@ -91,7 +87,7 @@ export function PackageLink({
`PackageLink|rename|${activeVersion.name}|${newPackageName}`,
`[PackageLink] New package name "${newPackageName}" exists in API documentation version "${activeVersion.name}". Set package="${newPackageName}".`,
);
- return {children ?? defaultText};
+ return {children ?? defaultText};
}
if (manifest[packageName] !== undefined) {
@@ -101,14 +97,14 @@ export function PackageLink({
`[PackageLink] Package "${packageName}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
);
}
- return {children ?? defaultText};
+ return {children ?? defaultText};
}
if (newApi) {
return {children ?? defaultText};
}
- return {children ?? defaultText};
+ return {children ?? defaultText};
}
/**
@@ -159,13 +155,6 @@ export interface ApiLinkProps<
* @remarks Remove this prop when the API documentation is available.
*/
newApi?: boolean;
-
- /**
- * Overrides the generated heading ID for the target API item.
- *
- * @deprecated Use a qualified {@link ApiLinkProps.api} reference to link directly to a member.
- */
- headingId?: string;
}
/**
@@ -177,7 +166,6 @@ export function ApiLink): JSX.Element {
const { activeVersion, manifest } = useApiLinkContext("ApiLink", true);
@@ -203,7 +191,6 @@ export function ApiLink{children};
}
diff --git a/website/test/unit/shortLinks.test.ts b/website/test/unit/shortLinks.test.ts
index c8a075463eb..4ff451d68df 100644
--- a/website/test/unit/shortLinks.test.ts
+++ b/website/test/unit/shortLinks.test.ts
@@ -253,22 +253,6 @@ describe("ApiLink", () => {
});
});
- it("allows a compatibility heading to override the manifest heading", () => {
- useVersion("current", "/docs");
- useMockApiLinkManifests();
-
- expect(
- renderApiLink({
- package: "example",
- api: "(Widget:class).run",
- headingId: "legacy-heading",
- }),
- ).toEqual({
- href: "/docs/api/example/widget-class#legacy-heading",
- children: "Widget.run",
- });
- });
-
it("renders inline code with rich children when a new API is not documented", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
From 0b240d8c6b19da0aa4af105613aec52fa81c6701 Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:35:06 +0000
Subject: [PATCH 05/11] remove: Design doc
---
website/STAGED_API_LINKS_DESIGN.md | 470 -----------------------------
1 file changed, 470 deletions(-)
delete mode 100644 website/STAGED_API_LINKS_DESIGN.md
diff --git a/website/STAGED_API_LINKS_DESIGN.md b/website/STAGED_API_LINKS_DESIGN.md
deleted file mode 100644
index 3641046d477..00000000000
--- a/website/STAGED_API_LINKS_DESIGN.md
+++ /dev/null
@@ -1,470 +0,0 @@
-# API link transitions design
-
-## Summary
-
-Add two transition mechanisms to `PackageLink` and `ApiLink`:
-
-- A `newApi` prop permits a target to be absent from the active API documentation. The component renders inline code when the target is absent. When the target exists, the component renders a link and logs a warning that tells the author to remove `newApi`.
-- An object value for `ApiLink.api` gives `ApiLink` a preferred API reference in `new` and a fallback reference in `previous`. When `new` resolves, the component logs a warning that tells the author to make the new reference permanent.
-- An object value for `PackageLink.package` gives `PackageLink` the same rename behavior for package names. The object contains `previous` and `new` names.
-
-These mechanisms let a website change merge with its related API change. The website can then work before and after release artifacts contain the API change.
-
-## Background
-
-The website publishes from the `main` branch. Published API documentation models come from release branches. Therefore, website content on `main` can be newer than the API documentation that the website consumes.
-
-A link to a new API fails until a release branch publishes an API model that contains the API. A link update for a renamed API has the inverse problem. The old name works before the new model is published, and the new name works after publication. There is no single current `ApiLink` value that works in both states.
-
-## Goals
-
-The design has these goals:
-
-1. Permit documentation for a new package or API to merge before its API model is published.
-2. Permit documentation for a renamed API to work with both the old and new API models.
-3. Keep normal API links strict.
-4. Keep invalid declaration references and ambiguous references as build errors.
-5. Tell authors when a temporary transition prop is no longer necessary.
-6. Resolve all links against the active documentation version.
-7. Preserve the current output for existing component use.
-
-## Non-goals
-
-The design does not do these tasks:
-
-- It does not change API model publication or website deployment.
-- It does not permit arbitrary broken links.
-- It does not add a fallback URL supplied by an author.
-- It does not find renamed APIs automatically.
-- It does not make transition warnings fail the build.
-
-## Proposed component API
-
-### `PackageLink`
-
-Update `package` to accept a string or a staged rename object. Add the optional `newApi` prop:
-
-```tsx
-export interface PackageLinkRename {
- previous: string;
- new: string;
-}
-
-export interface PackageLinkProps {
- children?: ReactNode;
- package: string | PackageLinkRename;
- newApi?: boolean;
-}
-```
-
-An MDX file can link to a package that is not in the published model:
-
-```mdx
-
-```
-
-The package name remains unscoped. This rule is the same as the current rule.
-
-An MDX file can prepare for a package rename:
-
-```mdx
-
-```
-
-`previous` contains the fallback unscoped package name. `new` contains the preferred unscoped package name. When the transition is complete, the author replaces the object with the `new` string value.
-
-### `ApiLink`
-
-Update `api` to accept a declaration reference or a staged rename object. Add the optional `newApi` prop:
-
-```tsx
-export interface ApiLinkRename<
- TPreviousApiSelector extends string = string,
- TNewApiSelector extends string = string,
-> {
- previous: ApiDeclarationReference;
- new: ApiDeclarationReference;
-}
-
-export interface ApiLinkProps<
- TApiSelector extends string = string,
- TNewApiSelector extends string = string,
-> {
- children?: ReactNode;
- package: string;
- api: ApiDeclarationReference | ApiLinkRename;
- newApi?: boolean;
-}
-```
-
-An MDX file can refer to a new API:
-
-```mdx
-
-```
-
-Because `newApi` has the `boolean` type, MDX and JSX permit the shorthand `newApi`. This syntax is equivalent to `newApi={true}`. The shorthand is the preferred syntax.
-
-An MDX file can prepare for a rename:
-
-```mdx
-
-```
-
-Both object properties use the same declaration-reference grammar as a string `api` value. An author specifies an API item kind with a TSDoc selector. For example, `(NewApi:class)` selects a class and `(NewApi:interface).method` selects a method on an interface. A separate API-kind prop is not necessary.
-
-`previous` is the fallback reference. `new` is the preferred reference. When the transition is complete, the author replaces the object with the `new` string value.
-
-## Resolution behavior
-
-All checks use the manifest for the active Docusaurus documentation version. A target can exist in one documentation version and be absent from another version.
-
-### `PackageLink` behavior
-
-A normal `PackageLink` keeps its current behavior. It creates the package URL without a manifest lookup.
-
-A `PackageLink` with an object `package` value checks the `new` package first. If the new package does not exist, it uses `previous`. When the new package exists, the component links to it and logs a warning to make the new name permanent.
-
-The following table applies when `package` is a string:
-
-| `newApi` | Package exists | Result |
-| -------- | -------------- | ----------------------------------------------------------- |
-| Not set | Either state | Render the current link. Existing broken-link checks apply. |
-| `true` | No | Render the content as inline code. Do not throw. |
-| `true` | Yes | Render the link. Log a warning to remove `newApi`. |
-
-The following table applies when `package` is an object:
-
-| Replacement package exists | Original package exists | `newApi` | Result |
-| -------------------------- | ----------------------- | ------------ | ----------------------------------------------------------------------------- |
-| Yes | Either state | Either state | Link to `new`. Log a warning to replace the object with the new package name. |
-| No | Yes | Either state | Link to `previous`. Do not log a transition warning. |
-| No | No | `true` | Render inline code. Do not throw. |
-| No | No | Not set | Render the current original-package link. Existing broken-link checks apply. |
-
-If the object form and `newApi` are both present, rename resolution runs first. `newApi` controls only the final state in which neither package exists.
-
-### `ApiLink` behavior
-
-`ApiLink` resolves `new` first when `api` is an object. If the preferred reference is not found, it resolves `previous`.
-
-The following table applies when `api` is a string:
-
-| `api` target | `newApi` | Result |
-| -------------- | -------- | ------------------------------------------------ |
-| Exists | `true` | Link to `api`. Log a warning to remove `newApi`. |
-| Exists | Not set | Render the current link. |
-| Does not exist | `true` | Render inline code. Do not throw. |
-| Does not exist | Not set | Throw the current not-found error. |
-
-The following table applies when `api` is an object:
-
-| `new` target | `previous` target | `newApi` | Result |
-| -------------- | ----------------- | ------------ | ------------------------------------------------------------------------------ |
-| Exists | Either state | Either state | Link to `new`. Log a warning to replace the object with the new API reference. |
-| Does not exist | Exists | Either state | Link to `previous`. Do not log a transition warning. |
-| Does not exist | Does not exist | `true` | Render inline code. Do not throw. |
-| Does not exist | Does not exist | Not set | Throw the current not-found error for `previous`. |
-
-If the object form and `newApi` are both present, rename resolution runs first. `newApi` only controls the final state in which neither reference exists. This combination supports a new API that changes name before its first API documentation artifact is published.
-
-### Errors that remain strict
-
-A transition prop suppresses only a not-found result. The following conditions remain errors:
-
-- The declaration-reference syntax is invalid.
-- A selector is not supported.
-- A reference is ambiguous.
-- The component is outside a versioned Docusaurus document.
-- The active documentation version has no manifest.
-
-This rule prevents `newApi` from hiding author errors. A kind or overload selector that is valid but has no matching target is a not-found result. This result can use the transition behavior.
-
-## Display text
-
-The fallback output is a React `` element. This element uses the same inline-code styling as inline code in MDX. The component does not add literal backtick characters.
-
-Each component preserves explicit `children`, including rich React content:
-
-```tsx
-{children ?? defaultText}
-```
-
-For `PackageLink` with a string `package` value, `defaultText` is the package name.
-
-For `PackageLink` with an object `package` value, `defaultText` is always `new`. This rule applies when the component links to `previous`.
-
-For `ApiLink` with a string `api` value, `defaultText` is the selector-free dotted path from `api`. This behavior matches the current link text.
-
-For `ApiLink` with an object `api` value, `defaultText` is the selector-free dotted path for the target that resolves. It uses `previous` while the old target resolves and `new` after the new target resolves. If neither target resolves and `newApi` is present, the inline-code fallback uses `new`.
-
-For example, this source:
-
-```mdx
-
-```
-
-renders visible text `OldApi` and links to `OldApi` before publication. After publication, it renders visible text `NewApi` and links to `NewApi`.
-
-All transition states permit rich child content. The component puts the same child tree in the link or the inline-code fallback. Authors can use inline elements such as emphasis and API display formatting. Authors should not add an outer inline-code element because the fallback already creates one.
-
-## Resolution API changes
-
-The current `resolveApiLinkTarget` function throws the same not-found error for these cases:
-
-- The package or dotted path is absent.
-- A valid kind selector has no match.
-- A valid overload selector has no match.
-
-The component must distinguish these cases from invalid or ambiguous references. Do not inspect error-message text.
-
-Add a non-throwing not-found result to the resolver layer. One possible shape is:
-
-```ts
-export type ApiLinkResolution =
- | {
- readonly found: true;
- readonly target: ApiLinkManifestEntry;
- readonly defaultText: string;
- }
- | {
- readonly found: false;
- readonly defaultText: string;
- };
-
-export function tryResolveApiLinkTarget(
- manifest: Readonly,
- packageName: string,
- api: string,
-): ApiLinkResolution;
-```
-
-`tryResolveApiLinkTarget` parses and validates the reference first. It throws for invalid, unsupported, or ambiguous references. It returns `found: false` only when no candidate matches a valid reference.
-
-Keep `resolveApiLinkTarget` as the strict wrapper:
-
-```ts
-export function resolveApiLinkTarget(/* current parameters */): ResolvedApiLink {
- const result = tryResolveApiLinkTarget(/* current arguments */);
- if (!result.found) {
- throw new Error(/* current not-found message */);
- }
- return result;
-}
-```
-
-This structure preserves the existing strict function and its error messages. It also gives the components a typed result for transition behavior.
-
-The parser must expose selector-free `defaultText` even for a not-found reference. This data is necessary for new-API output and rename display text.
-
-## Warning behavior
-
-Use `console.warn` during server rendering. Docusaurus includes server-render output in local and continuous integration build logs.
-
-Warnings should use stable and actionable text. Proposed messages are:
-
-```text
-[PackageLink] Package "new-package" exists in API documentation version "current". Remove the newApi prop.
-```
-
-```text
-[ApiLink] API "fluid-framework/NewApi" exists in API documentation version "current". Remove the newApi prop.
-```
-
-```text
-[ApiLink] New API name "fluid-framework/NewApi" exists in API documentation version "current". Set api="NewApi".
-```
-
-```text
-[PackageLink] New package name "new-package" exists in API documentation version "current". Set package="new-package".
-```
-
-The actual message must preserve selector syntax when it tells the author which value to put in `api`.
-
-Use a module-level `Set` to reduce duplicate warnings in one process. Include these values in the warning key:
-
-- Component type
-- Active documentation version
-- Package name
-- API reference, when applicable
-- Transition prop type
-
-Only emit these warnings when `typeof window === "undefined"`. This rule prevents every reader from receiving author diagnostics in the browser console. Parallel Docusaurus workers can still produce more than one copy of a warning. Duplicate prevention is best effort and must not affect correctness.
-
-Warnings are best-effort build messages. They must not fail the build. A later change can add a stricter cleanup mechanism if the team needs one.
-
-## Implementation plan
-
-### 1. Refactor API reference resolution
-
-Update `website/src/apiLinkReference.ts`:
-
-1. Separate reference parsing from candidate lookup.
-2. Add `tryResolveApiLinkTarget` and `ApiLinkResolution`.
-3. Return selector-free display text for found and not-found results.
-4. Keep `resolveApiLinkTarget` as a strict compatibility wrapper.
-5. Keep current ambiguity, parser, and selector errors.
-
-### 2. Share active-version manifest lookup
-
-Update `website/src/components/shortLinks.tsx`:
-
-1. Add a helper that gets the active version and its manifest.
-2. Use this helper in `ApiLink`.
-3. Use this helper in `PackageLink` when `newApi` is true or `package` is an object.
-4. Preserve the current normal `PackageLink` path construction.
-
-The helper must keep the current errors for a missing version context and a missing manifest. Error text can name the calling component.
-
-### 3. Add transition rendering
-
-Update `PackageLink` and `ApiLink`:
-
-1. Add the new props and TSDoc comments.
-2. Implement the resolution tables in this document.
-3. Render `` for the permitted final not-found state.
-4. Keep explicit children unchanged.
-5. Use the preferred new reference or package name for default display text.
-6. Preserve rich child content in link and inline-code output.
-
-### 4. Add warning support
-
-Add a small private warning helper in `shortLinks.tsx`, or in a nearby module if tests need direct access. The helper emits server-only, best-effort deduplicated warnings.
-
-Do not add warning data to the generated manifest. The warning depends on component props, not on API model metadata.
-
-### 5. Add documentation examples
-
-Add contributor guidance for these transition values after implementation. State that `newApi` and the rename object forms are temporary. Include the required cleanup operation for each warning.
-
-Do not add a new-API example to published product documentation only to test the component. Use unit tests for transition states.
-
-## Test plan
-
-Extend `website/test/unit/shortLinks.test.ts` and the resolver unit tests.
-
-### Type tests
-
-Verify these cases:
-
-- `newApi` is accepted by both components with JSX boolean shorthand.
-- `api` accepts an object with valid `previous` and `new` declaration references.
-- The object properties accept kind selectors.
-- The object properties reject invalid literal references.
-- `package` accepts a string or an object with string `previous` and `new` properties.
-- Existing `api` type checks continue to work.
-
-### `PackageLink` tests
-
-Verify these cases:
-
-- Normal links keep their current output and do not require manifest data.
-- A missing new package renders `` with default text when `newApi` is present.
-- A missing new package preserves explicit children when `newApi` is present.
-- An existing package renders a link when `newApi` is present.
-- An existing package logs the cleanup warning when `newApi` is present.
-- The warning includes the active version.
-- A missing `new` package falls back to `previous` without a warning.
-- An existing `new` package wins and logs a cleanup warning.
-- Default text uses `new` before and after the package target exists.
-- Explicit rich children remain unchanged before and after the target exists.
-- If both package names are absent, `newApi` controls code output versus the current original-package link.
-
-### `ApiLink` tests
-
-Verify these cases:
-
-- A missing API renders `` when `newApi` is present.
-- A missing API preserves explicit children when `newApi` is present.
-- An existing API renders a link and logs the cleanup warning when `newApi` is present.
-- A missing `new` target falls back to `previous` without a warning.
-- An existing `new` target wins and logs a cleanup warning.
-- Default text uses the API reference that resolves.
-- Explicit rich children remain unchanged before and after the target exists.
-- If both references are absent, `newApi` controls code output versus an error.
-- A malformed preferred reference throws and does not fall back.
-- An ambiguous preferred reference throws and does not fall back.
-- A valid preferred selector with no match falls back.
-- A valid preferred overload with no match falls back.
-- Existing version, anchor, overload, and selector tests continue to pass.
-
-### Warning tests
-
-Mock `console.warn` and server rendering. Reset the warning-key set between tests through a test-only reset function or module reset. Verify warning content and deduplication.
-
-Do not depend only on browser tests for warnings. The warnings are server diagnostics.
-
-### Build validation
-
-Run these website checks:
-
-1. Unit tests.
-2. TypeScript test compilation.
-3. ESLint and Prettier checks.
-4. A full Docusaurus build with generated API documentation.
-
-The full build confirms that `` output works in MDX server rendering and that warnings appear in build logs.
-
-## Compatibility
-
-The new props are optional. Existing MDX source and rendered output do not change.
-
-The strict `resolveApiLinkTarget` export keeps its current behavior. New code uses `tryResolveApiLinkTarget` only when it needs transition behavior.
-
-The generated manifest format does not change. Existing generated manifests remain valid.
-
-The feature works independently for each active documentation version. A link with `newApi` can render as code in one version and as a link in another version. A warning identifies the version in which cleanup is possible.
-
-## Alternatives considered
-
-### Catch all resolver errors in the component
-
-This option is small, but it can hide malformed and ambiguous references. It also depends on thrown error text to identify not-found results. Reject this option.
-
-### Add a replacement URL
-
-An author-supplied URL bypasses manifest validation and version handling. It can become stale. Reject this option.
-
-### Put replacement aliases in the generated manifest
-
-The API models do not contain the documentation author's old-to-new mapping. An alias file would add a separate data source and cleanup process. A component prop keeps the transition next to the affected content. Reject this option for the first implementation.
-
-### Render plain text instead of inline code
-
-Plain text does not identify an API symbol as clearly as existing MDX inline code. The requested fallback is inline code. Reject this option.
-
-### Make a successful new-API link fail the build
-
-A build error would force immediate cleanup, but artifact updates can occur without a matching website change. This behavior would recreate the current urgent failure. Use a warning instead.
-
-## Cleanup workflow
-
-For a new API or package:
-
-1. Add the link with `newApi` in the website change that documents the new item.
-2. The website renders inline code until the artifact contains the item.
-3. A later website build logs a warning after the artifact contains the item.
-4. Remove `newApi` in a cleanup change.
-
-For a renamed API:
-
-1. Put the old qualified reference in `api.previous`.
-2. Put the new qualified reference in `api.new`.
-3. The website displays the old name and links to the old API documentation before publication when child content is omitted.
-4. The website links to the new API documentation and logs a warning after publication.
-5. Replace the `api` object with the value from `api.new`.
-
-For a renamed package:
-
-1. Put the old unscoped package name in `package.previous`.
-2. Put the new unscoped package name in `package.new`.
-3. The website displays the new package name and links to the old package documentation before publication.
-4. The website links to the new package documentation and logs a warning after publication.
-5. Replace the `package` object with the value from `package.new`.
-
-## Resolved design decisions
-
-1. Use the object form of `ApiLink.api` for API rename transitions.
-2. Use best-effort build warnings. Do not add stricter cleanup enforcement now.
-3. Support package rename transitions with the object form of `PackageLink.package` in the first implementation.
-4. Permit rich child content in all link and fallback states.
From b99f42f7d9398ff62f34bf2557338a56eb8150da Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Thu, 3 Sep 2026 18:02:33 +0000
Subject: [PATCH 06/11] docs: Update document to leverage new functionality
---
.../tree/schema-evolution/feature-flag-schema-upgrades.mdx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/website/docs/data-structures/tree/schema-evolution/feature-flag-schema-upgrades.mdx b/website/docs/data-structures/tree/schema-evolution/feature-flag-schema-upgrades.mdx
index 265baa27bd4..8063c1aa8a1 100644
--- a/website/docs/data-structures/tree/schema-evolution/feature-flag-schema-upgrades.mdx
+++ b/website/docs/data-structures/tree/schema-evolution/feature-flag-schema-upgrades.mdx
@@ -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 's `isStagedUpgradeEnabled` property to check whether a document has already been upgraded and conditionally include that upgrade token for those documents.
+You can use to check whether a document has already been upgraded and conditionally include that upgrade token for those documents.
Disabling the feature flag prevents **new** documents from being upgraded, but documents that have already been upgraded retain the new schema.
This means:
From 927ff2e3c0f278c967956430b5fcf6f3f045416f Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Thu, 3 Sep 2026 19:00:20 +0000
Subject: [PATCH 07/11] improvement: Fallback logging
---
website/README.md | 3 +++
website/src/components/shortLinks.tsx | 35 +++++++++++++++++++++---
website/test/unit/shortLinks.test.ts | 39 +++++++++++++++++++++++++++
3 files changed, 73 insertions(+), 4 deletions(-)
diff --git a/website/README.md b/website/README.md
index e1bda63ffda..8f6b57e0a00 100644
--- a/website/README.md
+++ b/website/README.md
@@ -177,6 +177,7 @@ See and ();
+const emittedTransitionDiagnostics = new Set();
/**
* The package names used while API documentation transitions through a package rename.
@@ -96,11 +96,20 @@ export function PackageLink({
`PackageLink|newApi|${activeVersion.name}|${packageName}`,
`[PackageLink] Package "${packageName}" exists in API documentation version "${activeVersion.name}". Remove the newApi prop.`,
);
+ } else if (newPackageName !== undefined) {
+ debugOnce(
+ `PackageLink|rename-fallback|${activeVersion.name}|${packageName}|${newPackageName}`,
+ `[PackageLink] New package name "${newPackageName}" does not exist in API documentation version "${activeVersion.name}". Linking to previous package "${packageName}".`,
+ );
}
return {children ?? defaultText};
}
if (newApi) {
+ debugOnce(
+ `PackageLink|code-fallback|${activeVersion.name}|${defaultText}`,
+ `[PackageLink] New package "${defaultText}" does not exist in API documentation version "${activeVersion.name}". Rendering inline code placeholder.`,
+ );
return {children ?? defaultText};
}
@@ -197,6 +206,11 @@ export function ApiLink{children ?? replacementResult?.defaultText ?? result.defaultText};
}
throw new Error(`No API documentation found for "${packageName}/${api}".`);
@@ -207,6 +221,11 @@ export function ApiLink void): void {
+ if (typeof window !== "undefined" || emittedTransitionDiagnostics.has(key)) {
return;
}
- emittedTransitionWarnings.add(key);
- console.warn(message);
+ emittedTransitionDiagnostics.add(key);
+ log(message);
}
/**
diff --git a/website/test/unit/shortLinks.test.ts b/website/test/unit/shortLinks.test.ts
index 4ff451d68df..064dbd6eb1d 100644
--- a/website/test/unit/shortLinks.test.ts
+++ b/website/test/unit/shortLinks.test.ts
@@ -127,6 +127,7 @@ describe("PackageLink", () => {
it("renders inline code when a new package is not documented", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
const link = PackageLink({ package: "new-package", newApi: true });
@@ -134,6 +135,9 @@ describe("PackageLink", () => {
type: "code",
children: "new-package",
});
+ expect(debug).toHaveBeenCalledWith(
+ '[PackageLink] New package "new-package" does not exist in API documentation version "current". Rendering inline code placeholder.',
+ );
});
it("preserves rich children when a new package is not documented", () => {
@@ -170,6 +174,7 @@ describe("PackageLink", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
const link = PackageLink({
package: { previous: "example", new: "replacement" },
@@ -180,6 +185,9 @@ describe("PackageLink", () => {
children: "replacement",
});
expect(warn).not.toHaveBeenCalled();
+ expect(debug).toHaveBeenCalledWith(
+ '[PackageLink] New package name "replacement" does not exist in API documentation version "current". Linking to previous package "example".',
+ );
});
it("uses and warns about a documented replacement package", () => {
@@ -199,6 +207,25 @@ describe("PackageLink", () => {
'[PackageLink] New package name "example" exists in API documentation version "current". Set package="example".',
);
});
+
+ it("renders inline code when neither package rename target is documented", () => {
+ useVersion("current", "/docs");
+ useMockApiLinkManifests();
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
+
+ const link = PackageLink({
+ package: { previous: "old-package", new: "renamed-package" },
+ newApi: true,
+ });
+
+ expect({ type: link.type, children: link.props.children }).toEqual({
+ type: "code",
+ children: "renamed-package",
+ });
+ expect(debug).toHaveBeenCalledWith(
+ '[PackageLink] New package "renamed-package" does not exist in API documentation version "current". Rendering inline code placeholder.',
+ );
+ });
});
describe("ApiLink", () => {
@@ -257,11 +284,15 @@ describe("ApiLink", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
const children = createElement("strong", undefined, "New API");
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
const link = ApiLink({ package: "example", api: "Missing", newApi: true, children });
expect(link.type).toBe("code");
expect(link.props.children).toBe(children);
+ expect(debug).toHaveBeenCalledWith(
+ '[ApiLink] New API "example/Missing" does not exist in API documentation version "current". Rendering inline code placeholder.',
+ );
});
it("links and warns when a new API is documented", () => {
@@ -284,6 +315,7 @@ describe("ApiLink", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
const link = renderApiLink({
package: "example",
@@ -295,6 +327,9 @@ describe("ApiLink", () => {
children: "Widget",
});
expect(warn).not.toHaveBeenCalled();
+ expect(debug).toHaveBeenCalledWith(
+ '[ApiLink] New API name "example/Replacement" does not exist in API documentation version "current". Linking to previous API "example/(Widget:class)".',
+ );
});
it("preserves rich children while an API rename is staged", () => {
@@ -315,6 +350,7 @@ describe("ApiLink", () => {
it("renders inline code when neither API rename target is documented", () => {
useVersion("current", "/docs");
useMockApiLinkManifests();
+ const debug = vi.spyOn(console, "debug").mockImplementation(() => {});
const link = ApiLink({
package: "example",
@@ -326,6 +362,9 @@ describe("ApiLink", () => {
type: "code",
children: "NewMissing",
});
+ expect(debug).toHaveBeenCalledWith(
+ '[ApiLink] New API "example/NewMissing" does not exist in API documentation version "current". Rendering inline code placeholder.',
+ );
});
it("uses and warns about a documented replacement API", () => {
From d25d8d2c03fc1ad8ef2f02239062a7774fefa139 Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Tue, 8 Sep 2026 13:18:14 -0700
Subject: [PATCH 08/11] docs: Remove invalid link
---
website/README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/website/README.md b/website/README.md
index 8f6b57e0a00..dcfa5e3a610 100644
--- a/website/README.md
+++ b/website/README.md
@@ -234,7 +234,6 @@ The component preserves the child content when it renders a link or inline code:
Transition behavior is version-specific.
A component can render inline code or use its old target in one documentation version and use its new target in another version.
-For implementation details and the complete behavior tables, see the [API link transitions design](./STAGED_API_LINKS_DESIGN.md).
#### Comments
From 9b4c866ae9541b95fef2b4f861c5182de1508e98 Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Tue, 8 Sep 2026 13:21:21 -0700
Subject: [PATCH 09/11] docs: Simplify notes
Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
---
website/README.md | 6 ------
1 file changed, 6 deletions(-)
diff --git a/website/README.md b/website/README.md
index dcfa5e3a610..1ca35435955 100644
--- a/website/README.md
+++ b/website/README.md
@@ -199,10 +199,7 @@ For an API rename, set `api` to an object with `previous` and `new` declaration
The component tries `new` first.
It uses `previous` while the published model contains only the old API.
-It writes a build debug message when it uses `previous`.
When child content is omitted, the component displays the name of the API that resolves.
-When `new` exists, the component writes a build warning.
-Replace the object with the new declaration reference at that time:
```mdx
@@ -215,9 +212,6 @@ For a package rename, set `package` to an object with `previous` and `new` names
```
The component uses `previous` while the published model contains only the old package.
-It writes a build debug message when it uses `previous`.
-When the `new` package exists, the component writes a build warning.
-Replace the object with the new string value at that time:
```mdx
From 83e23b5b137297b446ef07e6a8180786606fd957 Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Tue, 8 Sep 2026 13:21:43 -0700
Subject: [PATCH 10/11] docs: Simplify notes
---
website/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/website/README.md b/website/README.md
index 1ca35435955..1607fe14f8a 100644
--- a/website/README.md
+++ b/website/README.md
@@ -178,8 +178,8 @@ See and
From d72511653542681d546b39d561a6f06d793c99c7 Mon Sep 17 00:00:00 2001
From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
Date: Tue, 8 Sep 2026 13:22:42 -0700
Subject: [PATCH 11/11] docs: Remove unnecessary notes
---
website/README.md | 3 ---
1 file changed, 3 deletions(-)
diff --git a/website/README.md b/website/README.md
index 1607fe14f8a..99bce7b665b 100644
--- a/website/README.md
+++ b/website/README.md
@@ -226,9 +226,6 @@ The component preserves the child content when it renders a link or inline code:
```
-Transition behavior is version-specific.
-A component can render inline code or use its old target in one documentation version and use its new target in another version.
-
#### Comments
A common pattern for adding inline comments in `.md` files looks like: