From f0b1061791cda8a481059e62d62a4bbc6feb418e Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Tue, 20 Jan 2026 22:16:53 +0100 Subject: [PATCH] docs(docs-infra): Handle additional description format Ex: https://angular.dev/api/router/withExperimentalPlatformNavigation --- .../api-gen/rendering/entities/traits.mts | 4 + .../templates/function-reference.tsx | 15 +- .../api-gen/rendering/test/marked.spec.mts | 8 +- .../test/transforms/jsdoc-transforms.spec.mts | 407 +++++++++--------- .../transforms/function-transforms.mts | 7 +- .../rendering/transforms/jsdoc-transforms.mts | 19 +- .../src/ngtsc/docs/src/jsdoc_extractor.ts | 2 + packages/router/src/provide_router.ts | 28 +- 8 files changed, 268 insertions(+), 222 deletions(-) diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities/traits.mts b/adev/shared-docs/pipeline/api-gen/rendering/entities/traits.mts index 190a5697a1b..6efc2724607 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities/traits.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities/traits.mts @@ -22,6 +22,10 @@ export interface HasJsDocTags { jsdocTags: JsDocTagEntry[]; } +export interface MaybeJsDocTags { + jsdocTags?: JsDocTagEntry[]; +} + export interface HasAdditionalLinks { additionalLinks: LinkEntryRenderable[]; } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx index e1aa46b849b..408f9baa769 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx @@ -73,13 +73,14 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
- {entry.signatures.map((s, i) => - signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), { - id: `${s.name}_${i}`, - printSignaturesAsHeader, - hideUsageNotes: true, - }), - )} + {entry.signatures.length > 1 && + entry.signatures.map((s, i) => + signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), { + id: `${s.name}_${i}`, + printSignaturesAsHeader, + hideUsageNotes: true, + }), + )}
diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts index 2c2a0cf9c2a..a3ecd04d120 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts @@ -8,12 +8,12 @@ import {readFile} from 'fs/promises'; import {JSDOM} from 'jsdom'; -import {getRenderable} from '../processing.mjs'; -import {renderEntry} from '../rendering.mjs'; -import {setSymbols} from '../symbol-context.mjs'; import {resolve} from 'path'; import {initHighlighter} from '../../../shared/shiki.mjs'; +import {getRenderable} from '../processing.mjs'; +import {renderEntry} from '../rendering.mjs'; import {setHighlighterInstance} from '../shiki/shiki.mjs'; +import {setSymbols} from '../symbol-context.mjs'; // Note: The tests will probably break if the schema of the api extraction changes. // All entries in the fake-entries are extracted from Angular's api. @@ -69,7 +69,7 @@ describe('markdown to html', () => { it('should render multiple {@link} blocks', () => { const provideClientHydrationEntry = entries.get('provideClientHydration')!; expect(provideClientHydrationEntry).toBeDefined(); - const cardItem = provideClientHydrationEntry.querySelector('.docs-reference-card-item')!; + const cardItem = provideClientHydrationEntry.querySelector('.docs-api')!; expect(cardItem.innerHTML).not.toContain('@link'); }); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts index bcf7f022703..285345badc6 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts @@ -17,204 +17,207 @@ import { // @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context. describe('jsdoc transforms', () => { - it('should transform links', () => { - setCurrentSymbol('Router'); - setSymbols( - Object.fromEntries([ - ['Route', 'test'], - ['Router', 'test'], - ['Router.someMethod', 'test'], - ['Router.someMethodWithParenthesis', 'test'], - ['FormGroup', 'test'], - ['FormGroup.someMethod', 'test'], - ]), - ); + describe('addHtmlAdditionalLinks', () => { + it('should transform links', () => { + setCurrentSymbol('Router'); + setSymbols( + Object.fromEntries([ + ['Route', 'test'], + ['Router', 'test'], + ['Router.someMethod', 'test'], + ['Router.someMethodWithParenthesis', 'test'], + ['FormGroup', 'test'], + ['FormGroup.someMethod', 'test'], + ]), + ); - const entry = addHtmlAdditionalLinks({ - jsdocTags: [ - { - name: 'see', - comment: '[Angular](https://angular.io)', - }, - { - name: 'see', - comment: '[Angular](https://angular.io "Angular")', - }, - { - name: 'see', - comment: '{@link Route}', - }, - { - name: 'see', - comment: '{@link Route Something else}', - }, - { - name: 'see', - comment: '{@link #someMethod}', - }, - { - name: 'see', - comment: '{@link #someMethodWithParenthesis()}', - }, - { - name: 'see', - comment: '{@link someMethod()}', - }, - { - name: 'see', - comment: '{@link FormGroup.someMethod()}', - }, - { - name: 'see', - comment: '{@link https://angular.dev/api/core/ApplicationRef}', - }, - { - name: 'see', - comment: '{@link https://angular.dev}', - }, - { - name: 'see', - comment: '{@link /cli/build ng build}', - }, - { - name: 'see', - comment: '{@link /ecosystem/rxjs-interop/output-interop Output Interop}', - }, - ], - moduleName: 'test', - }); - - expect(entry.additionalLinks[0]).toEqual({ - label: 'Angular', - url: 'https://angular.io', - title: undefined, - }); - - expect(entry.additionalLinks[1]).toEqual({ - label: 'Angular', - url: 'https://angular.io', - title: 'Angular', - }); - - expect(entry.additionalLinks[2]).toEqual({ - label: 'Route', - url: '/api/test/Route', - }); - - expect(entry.additionalLinks[3]).toEqual({ - label: 'Something else', - url: '/api/test/Route', - }); - - expect(entry.additionalLinks[4]).toEqual({ - label: 'someMethod', - url: '/api/test/Router#someMethod', - }); - expect(entry.additionalLinks[5]).toEqual({ - label: 'someMethodWithParenthesis()', - url: '/api/test/Router#someMethodWithParenthesis', - }); - expect(entry.additionalLinks[6]).toEqual({ - label: 'someMethod()', - url: '/api/test/Router#someMethod', - }); - expect(entry.additionalLinks[7]).toEqual({ - label: 'FormGroup.someMethod()', - url: '/api/test/FormGroup#someMethod', - }); - - expect(entry.additionalLinks[8]).toEqual({ - label: 'ApplicationRef', - url: 'https://angular.dev/api/core/ApplicationRef', - }); - expect(entry.additionalLinks[9]).toEqual({ - label: 'angular.dev', - url: 'https://angular.dev', - }); - - expect(entry.additionalLinks[10]).toEqual({ - label: 'ng build', - url: '/cli/build', - }); - - expect(entry.additionalLinks[11]).toEqual({ - label: 'Output Interop', - url: '/ecosystem/rxjs-interop/output-interop', - }); - }); - - it('should convert backticks to code tags in markdown links', () => { - const entry = addHtmlAdditionalLinks({ - jsdocTags: [ - { - name: 'see', - comment: - '[Host view using `ViewContainerRef.createComponent`](guide/components/programmatic-rendering#host-view-using-viewcontainerrefcreatecomponent)', - }, - { - name: 'see', - comment: - '[Popup attached to `document.body` with `createComponent` + `hostElement`](guide/components/programmatic-rendering#popup-attached-to-documentbody-with-createcomponent--hostelement)', - }, - { - name: 'see', - comment: '[Method with `backticks` in title](https://example.com "Title with `code`")', - }, - ], - moduleName: 'test', - }); - - expect(entry.additionalLinks[0]).toEqual({ - label: 'Host view using ViewContainerRef.createComponent', - url: 'guide/components/programmatic-rendering#host-view-using-viewcontainerrefcreatecomponent', - title: undefined, - }); - - expect(entry.additionalLinks[1]).toEqual({ - label: - 'Popup attached to document.body with createComponent + hostElement', - url: 'guide/components/programmatic-rendering#popup-attached-to-documentbody-with-createcomponent--hostelement', - title: undefined, - }); - - expect(entry.additionalLinks[2]).toEqual({ - label: 'Method with backticks in title', - url: 'https://example.com', - title: 'Title with `code`', - }); - }); - - it('should throw on invalid relatie @link', () => { - const entryFn = () => - addHtmlAdditionalLinks({ + const entry = addHtmlAdditionalLinks({ jsdocTags: [ { name: 'see', - comment: '{@link cli/build ng build}', + comment: '[Angular](https://angular.io)', + }, + { + name: 'see', + comment: '[Angular](https://angular.io "Angular")', + }, + { + name: 'see', + comment: '{@link Route}', + }, + { + name: 'see', + comment: '{@link Route Something else}', + }, + { + name: 'see', + comment: '{@link #someMethod}', + }, + { + name: 'see', + comment: '{@link #someMethodWithParenthesis()}', + }, + { + name: 'see', + comment: '{@link someMethod()}', + }, + { + name: 'see', + comment: '{@link FormGroup.someMethod()}', + }, + { + name: 'see', + comment: '{@link https://angular.dev/api/core/ApplicationRef}', + }, + { + name: 'see', + comment: '{@link https://angular.dev}', + }, + { + name: 'see', + comment: '{@link /cli/build ng build}', + }, + { + name: 'see', + comment: '{@link /ecosystem/rxjs-interop/output-interop Output Interop}', }, ], moduleName: 'test', }); - expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/); + expect(entry.additionalLinks[0]).toEqual({ + label: 'Angular', + url: 'https://angular.io', + title: undefined, + }); + + expect(entry.additionalLinks[1]).toEqual({ + label: 'Angular', + url: 'https://angular.io', + title: 'Angular', + }); + + expect(entry.additionalLinks[2]).toEqual({ + label: 'Route', + url: '/api/test/Route', + }); + + expect(entry.additionalLinks[3]).toEqual({ + label: 'Something else', + url: '/api/test/Route', + }); + + expect(entry.additionalLinks[4]).toEqual({ + label: 'someMethod', + url: '/api/test/Router#someMethod', + }); + expect(entry.additionalLinks[5]).toEqual({ + label: 'someMethodWithParenthesis()', + url: '/api/test/Router#someMethodWithParenthesis', + }); + expect(entry.additionalLinks[6]).toEqual({ + label: 'someMethod()', + url: '/api/test/Router#someMethod', + }); + expect(entry.additionalLinks[7]).toEqual({ + label: 'FormGroup.someMethod()', + url: '/api/test/FormGroup#someMethod', + }); + + expect(entry.additionalLinks[8]).toEqual({ + label: 'ApplicationRef', + url: 'https://angular.dev/api/core/ApplicationRef', + }); + expect(entry.additionalLinks[9]).toEqual({ + label: 'angular.dev', + url: 'https://angular.dev', + }); + + expect(entry.additionalLinks[10]).toEqual({ + label: 'ng build', + url: '/cli/build', + }); + + expect(entry.additionalLinks[11]).toEqual({ + label: 'Output Interop', + url: '/ecosystem/rxjs-interop/output-interop', + }); + }); + + it('should convert backticks to code tags in markdown links', () => { + const entry = addHtmlAdditionalLinks({ + jsdocTags: [ + { + name: 'see', + comment: + '[Host view using `ViewContainerRef.createComponent`](guide/components/programmatic-rendering#host-view-using-viewcontainerrefcreatecomponent)', + }, + { + name: 'see', + comment: + '[Popup attached to `document.body` with `createComponent` + `hostElement`](guide/components/programmatic-rendering#popup-attached-to-documentbody-with-createcomponent--hostelement)', + }, + { + name: 'see', + comment: '[Method with `backticks` in title](https://example.com "Title with `code`")', + }, + ], + moduleName: 'test', + }); + + expect(entry.additionalLinks[0]).toEqual({ + label: 'Host view using ViewContainerRef.createComponent', + url: 'guide/components/programmatic-rendering#host-view-using-viewcontainerrefcreatecomponent', + title: undefined, + }); + + expect(entry.additionalLinks[1]).toEqual({ + label: + 'Popup attached to document.body with createComponent + hostElement', + url: 'guide/components/programmatic-rendering#popup-attached-to-documentbody-with-createcomponent--hostelement', + title: undefined, + }); + + expect(entry.additionalLinks[2]).toEqual({ + label: 'Method with backticks in title', + url: 'https://example.com', + title: 'Title with `code`', + }); + }); + + it('should throw on invalid relatie @link', () => { + const entryFn = () => + addHtmlAdditionalLinks({ + jsdocTags: [ + { + name: 'see', + comment: '{@link cli/build ng build}', + }, + ], + moduleName: 'test', + }); + + expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/); + }); }); - it('should parse markdown in descriptions', async () => { - setHighlighterInstance(await initHighlighter()); + describe('addHtmlDescription', () => { + it('should parse markdown in descriptions', async () => { + setHighlighterInstance(await initHighlighter()); - setSymbols( - Object.fromEntries([ - ['Route', 'test'], - ['Router', 'angular/router'], - ['Router.someMethod', 'test'], - ['Router.someMethodWithParenthesis', 'test'], - ['FormGroup', 'test'], - ['FormGroup.someMethod', 'test'], - ]), - ); + setSymbols( + Object.fromEntries([ + ['Route', 'test'], + ['Router', 'angular/router'], + ['Router.someMethod', 'test'], + ['Router.someMethodWithParenthesis', 'test'], + ['FormGroup', 'test'], + ['FormGroup.someMethod', 'test'], + ]), + ); - const entry = addHtmlDescription({ - description: ` + const entry = addHtmlDescription({ + description: ` \`\`\`angular-ts import { Router } from '@angular/router'; @@ -223,16 +226,36 @@ function setupRouter() { } \`\`\` `, - moduleName: 'test', + moduleName: 'test', + }); + + // Should have some shiki variables (meaning the description was highlighted). + expect(entry.htmlDescription).toContain('--shiki'); + + // Having docs-code means that the description was parsed and formatted correctly (by the shared marked renderer) + expect(entry.htmlDescription).toContain('class="docs-code"'); + + expect(entry.htmlDescription).toContain('/api/angular/router/Router'); }); - // Should have some shiki variables (meaning the description was highlighted). - expect(entry.htmlDescription).toContain('--shiki'); + it('should transform entry with different description & description tag', () => { + const entry = addHtmlDescription({ + 'description': + "Enables interop with the browser's `Navigation` API for router navigations.", + 'jsdocTags': [ + { + 'name': 'description', + 'comment': 'This feature is _highly_ experimental ...', + }, + ], + 'moduleName': 'platform-browser', + }); - // Having docs-code means that the description was parsed and formatted correctly (by the shared marked renderer) - expect(entry.htmlDescription).toContain('class="docs-code"'); - - expect(entry.htmlDescription).toContain('/api/angular/router/Router'); + expect(entry.htmlDescription).toBe(`

This feature is highly experimental ...

`); + expect(entry.shortHtmlDescription).toBe( + `

Enables interop with the browser's Navigation API for router navigations.

`, + ); + }); }); it('should only mark as deprecated if all overloads are deprecated', () => { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/function-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/function-transforms.mts index 46befb4cdbb..7b2468a0c45 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/function-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/function-transforms.mts @@ -29,7 +29,7 @@ export async function getFunctionRenderable( moduleName: string, repo: string, ): Promise { - return setEntryFlags( + const a = setEntryFlags( await addRenderableCodeToc( addHtmlAdditionalLinks( addHtmlUsageNotes( @@ -42,6 +42,11 @@ export async function getFunctionRenderable( ), ), ); + if (entry.name === 'withExperimentalPlatformNavigation') { + console.warn('**************', a); + } + + return a; } export function getFunctionMetadataRenderable( diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts index 4a49c2459f7..98f7822a2d1 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts @@ -22,6 +22,7 @@ import { HasModuleName, HasRenderableJsDocTags, HasStableFlag, + MaybeJsDocTags, } from '../entities/traits.mjs'; import {parseMarkdown} from '../../../shared/marked/parse.mjs'; @@ -43,7 +44,7 @@ const jsDoclinkRegex = /\{\s*@link\s+([^}]+)\s*\}/; const jsDoclinkRegexGlobal = new RegExp(jsDoclinkRegex.source, 'g'); /** Given an entity with a description, gets the entity augmented with an `htmlDescription`. */ -export function addHtmlDescription( +export function addHtmlDescription( entry: T, ): T & HasHtmlDescription { const firstParagraphRule = /(.*?)(?:\n\n|$)/s; @@ -56,10 +57,17 @@ export function addHtmlDescription( ?.comment ?? ''; } - const description = !!entry.description ? entry.description : jsDocDescription; - const shortTextMatch = description.match(firstParagraphRule); + let description = entry.description || jsDocDescription; + let shortDescription = description.match(firstParagraphRule)?.[0] ?? ''; + + // For the cases where the @description tag is after a short description + if (jsDocDescription && description !== jsDocDescription) { + shortDescription = entry.description; + description = jsDocDescription; + } + const htmlDescription = getHtmlForJsDocText(description).trim(); - const shortHtmlDescription = getHtmlForJsDocText(shortTextMatch ? shortTextMatch[0] : '').trim(); + const shortHtmlDescription = getHtmlForJsDocText(shortDescription).trim(); return {...entry, htmlDescription, shortHtmlDescription}; } @@ -137,6 +145,9 @@ function getHtmlAdditionalLinks(entry: T): LinkEntryRend .filter((tag) => tag.name === JS_DOC_SEE_TAG) .map((tag) => tag.comment) .map((comment) => { + // TODO: Throw when the comment is an absolute link. + // With TS 5.9 this is not possible as the ts api that extracts comments from tags strips the "http" part of links. + const markdownLinkMatch = comment.match(markdownLinkRule); if (markdownLinkMatch) { diff --git a/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts index 41fe4720539..96f39e1d1c7 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts @@ -24,6 +24,8 @@ export function extractJsDocTags(node: ts.HasJSDoc): JsDocTagEntry[] { return ts.getJSDocTags(escapedNode).map((t) => { return { name: t.tagName.getText(), + // In TS 5.9, ts.getTextOfJSDocComment still strips "http" from comments breaking any absolute links in @see blocks. + // eg: @see https://angular.dev comment: unescapeAngularDecorators(ts.getTextOfJSDocComment(t.comment) ?? ''), }; }); diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index 82e00d87bb6..4d265cf886b 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -8,10 +8,10 @@ import { HashLocationStrategy, + Location, LOCATION_INITIALIZED, LocationStrategy, ViewportScroller, - Location, ɵNavigationAdapterForLocation, } from '@angular/common'; import { @@ -23,15 +23,15 @@ import { inject, InjectionToken, Injector, + ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION as IS_ENABLED_BLOCKING_INITIAL_NAVIGATION, makeEnvironmentProviders, + ɵperformanceMarkFeature as performanceMarkFeature, provideAppInitializer, + provideEnvironmentInitializer, Provider, runInInjectionContext, - ɵperformanceMarkFeature as performanceMarkFeature, - ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION as IS_ENABLED_BLOCKING_INITIAL_NAVIGATION, - ɵpublishExternalGlobalUtil, - provideEnvironmentInitializer, Type, + ɵpublishExternalGlobalUtil, } from '@angular/core'; import {of, Subject} from 'rxjs'; @@ -39,15 +39,18 @@ import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outl import {Event, NavigationError, stringifyEvent} from './events'; import {RedirectCommand, Routes} from './models'; import {NAVIGATION_ERROR_HANDLER, NavigationTransitions} from './navigation_transition'; +import {ROUTE_INJECTOR_CLEANUP, routeInjectorCleanup} from './route_injector_cleanup'; import {Router} from './router'; import {InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config'; import {ROUTES} from './router_config_loader'; import {PreloadingStrategy, RouterPreloader} from './router_preloader'; -import {routeInjectorCleanup, ROUTE_INJECTOR_CLEANUP} from './route_injector_cleanup'; import {ROUTER_SCROLLER, RouterScroller} from './router_scroller'; +import {getLoadedRoutes, getRouterInstance, navigateByUrl} from './router_devtools'; import {ActivatedRoute} from './router_state'; +import {NavigationStateManager} from './statemanager/navigation_state_manager'; +import {StateManager} from './statemanager/state_manager'; import {afterNextNavigation} from './utils/navigations'; import { CREATE_VIEW_TRANSITION, @@ -55,9 +58,6 @@ import { VIEW_TRANSITION_OPTIONS, ViewTransitionsFeatureOptions, } from './utils/view_transition'; -import {getLoadedRoutes, getRouterInstance, navigateByUrl} from './router_devtools'; -import {StateManager} from './statemanager/state_manager'; -import {NavigationStateManager} from './statemanager/navigation_state_manager'; /** * Sets up providers necessary to enable `Router` functionality for the application. @@ -282,9 +282,9 @@ export type ExperimentalPlatformNavigationFeature = * }); * ``` * - * @see https://github.com/WICG/navigation-api?tab=readme-ov-file#problem-statement - * @see https://developer.chrome.com/docs/web-platform/navigation-api/ - * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API + * @see [Navigation API on WICG](https://github.com/WICG/navigation-api?tab=readme-ov-file#problem-statement) + * @see [Navigation API on Chrome from developers](https://developer.chrome.com/docs/web-platform/navigation-api/) + * @see [Navigation API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) * * @experimental 21.1 * @returns A `RouterFeature` that enables the platform navigation. @@ -899,8 +899,8 @@ export function withComponentInputBinding(): ComponentInputBindingFeature { * ``` * * @returns A set of providers for use with `provideRouter`. - * @see https://developer.chrome.com/docs/web-platform/view-transitions/ - * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API + * @see [View Transitions on MDN](https://developer.chrome.com/docs/web-platform/view-transitions/) + * @see [View Transitions API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) * @see [Route transition animations](guide/routing/route-transition-animations) * @developerPreview 19.0 */