diff --git a/adev/shared-docs/pipeline/api-gen/extraction/index.ts b/adev/shared-docs/pipeline/api-gen/extraction/index.ts index 4bc7d9b40bf..b30b7429461 100644 --- a/adev/shared-docs/pipeline/api-gen/extraction/index.ts +++ b/adev/shared-docs/pipeline/api-gen/extraction/index.ts @@ -7,6 +7,8 @@ import { createCompilerHost, DocEntry, EntryCollection, + InterfaceEntry, + ClassEntry, } from '@angular/compiler-cli'; import ts from 'typescript'; @@ -81,10 +83,28 @@ function main() { // Exported symbols from the current package ...apiDoc.entries.map((entry) => [entry.name, moduleName]), + + // Also doing it for every member of classes/interfaces + ...apiDoc.entries.flatMap((entry) => [ + [entry.name, moduleName], + ...getEntriesFromMembers(entry).map((member) => [member, moduleName]), + ]), ], } as EntryCollection); writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'}); } +function getEntriesFromMembers(entry: DocEntry): string[] { + if (!hasMembers(entry)) { + return []; + } + + return entry.members.map((member) => `${entry.name}.${member.name}`); +} + +function hasMembers(entry: DocEntry): entry is InterfaceEntry | ClassEntry { + return 'members' in entry; +} + main(); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/index.ts b/adev/shared-docs/pipeline/api-gen/rendering/index.ts index b8afd9d8115..5092dcf7a1b 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/index.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/index.ts @@ -7,7 +7,7 @@ import {configureMarkedGlobally} from './marked/configuration'; import {getRenderable} from './processing'; import {renderEntry} from './rendering'; import {initHighlighter} from './shiki/shiki'; -import {setSymbols} from './symbol-context'; +import {setCurrentSymbol, setSymbols} from './symbol-context'; /** The JSON data file format for extracted API reference info. */ interface EntryCollection { @@ -110,9 +110,10 @@ async function main() { // Setting the symbols are a global context for the rendering templates of this entry setSymbols(collection.symbols); - const renderableEntries = extractedEntries.map((entry) => - getRenderable(entry, collection.moduleName), - ); + const renderableEntries = extractedEntries.map((entry) => { + setCurrentSymbol(entry.name); + return getRenderable(entry, collection.moduleName); + }); const htmlOutputs = renderableEntries.map(renderEntry); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts b/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts index 07e3ae74714..b5d6fd67332 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts @@ -27,9 +27,11 @@ import {EnumReference} from './templates/enum-reference'; import {FunctionReference} from './templates/function-reference'; import {InitializerApiFunction} from './templates/initializer-api-function'; import {TypeAliasReference} from './templates/type-alias-reference'; +import {setCurrentSymbol} from './symbol-context'; /** Given a doc entry, get the transformed version of the entry for rendering. */ export function renderEntry(renderable: DocEntryRenderable | CliCommandRenderable): string { + setCurrentSymbol(renderable.name); if (isCliEntry(renderable)) { return render(CliCommandReference(renderable)); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.ts b/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.ts index 6ec08760778..806a04defcb 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.ts @@ -5,6 +5,9 @@ let symbols = new Map(); +// This is used to store the currently processed symbol (usually a class or an interface) +let currentSymbol: string | undefined; + export function setSymbols(newSymbols: Map): void { symbols = newSymbols; } @@ -12,7 +15,23 @@ export function setSymbols(newSymbols: Map): void { /** * Returns the module name of a symbol. * eg: 'ApplicationRef' => 'core', 'FormControl' => 'forms' + * Also supports class.member, 'NgZone.runOutsideAngular => 'core' */ export function getModuleName(symbol: string): string | undefined { - return symbols.get(symbol)?.replace('@angular/', ''); + const moduleName = symbols.get(symbol); + return moduleName?.replace('@angular/', ''); +} + +export function setCurrentSymbol(symbol: string): void { + currentSymbol = symbol; +} + +export function getCurrentSymbol(): string | undefined { + return currentSymbol; +} + +export function logUnknownSymbol(link: string, symbol: string): void { + console.warn( + `WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`, + ); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.ts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.ts index 72a84ba86ef..0eb4af5420f 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.ts @@ -1,10 +1,21 @@ -import {setSymbols} from '../../symbol-context'; +import {setCurrentSymbol, setSymbols} from '../../symbol-context'; import {addHtmlAdditionalLinks} from '../../transforms/jsdoc-transforms'; // @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context. describe('jsdoc transforms', () => { it('should transform links', () => { - setSymbols(new Map([['Route', 'test']])); + setCurrentSymbol('Router'); + setSymbols( + new Map([ + ['Route', 'test'], + ['Router', 'test'], + ['Router.someMethod', 'test'], + ['Router.someMethodWithParenthesis', 'test'], + ['FormGroup', 'test'], + ['FormGroup.someMethod', 'test'], + ]), + ); + const entry = addHtmlAdditionalLinks({ jsdocTags: [ { @@ -23,6 +34,30 @@ describe('jsdoc transforms', () => { 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}', + }, ], moduleName: 'test', }); @@ -48,5 +83,31 @@ describe('jsdoc transforms', () => { 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', + }); }); }); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.ts index aac86af1722..8134b4b2d03 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.ts @@ -31,7 +31,7 @@ import { import {getLinkToModule} from './url-transforms'; import {addApiLinksToHtml} from './code-transforms'; -import {getModuleName} from '../symbol-context'; +import {getCurrentSymbol, getModuleName, logUnknownSymbol} from '../symbol-context'; export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes'; export const JS_DOC_SEE_TAG = 'see'; @@ -197,27 +197,41 @@ function convertLinks(text: string) { }); } -function parseAtLink(link: string) { +function parseAtLink(link: string): {label: string; url: string} { // Because of microsoft/TypeScript/issues/59679 // getTextOfJSDocComment introduces an extra space between the symbol and a trailing () link = link.replace(/ \(\)$/, ''); let [rawSymbol, description] = link.split(/\s(.+)/); - let [symbol, subSymbol] = rawSymbol.split(/(?:#|\.)/); - - const moduleName = getModuleName(symbol)!; - if (!moduleName) { - logWarning(link, symbol); + if (rawSymbol.startsWith('#')) { + rawSymbol = rawSymbol.substring(1); + } else if (rawSymbol.startsWith('http://') || rawSymbol.startsWith('https://')) { + return { + url: rawSymbol, + label: rawSymbol.split('/').pop()!, + }; } - return { - label: description ?? rawSymbol, - url: getLinkToModule(moduleName, symbol, subSymbol), - }; -} + let [symbol, subSymbol] = rawSymbol.replace(/\(\)$/, '').split(/(?:#|\.)/); -function logWarning(link: string, symbol: string) { - // TODO: remove the links that generate this error - // TODO: throw an error when there are no more warning generated - console.warn(`WARNING: {@link ${link}} is invalid, ${symbol} is unknown in this context`); + let moduleName = getModuleName(symbol); + const label = description ?? rawSymbol; + + const currentSymbol = getCurrentSymbol(); + + if (!moduleName) { + // 2nd attemp, try to get the module name in the context of the current symbol + moduleName = getModuleName(`${currentSymbol}.${symbol}`); + + if (!moduleName || !currentSymbol) { + // TODO: remove the links that generate this error + // TODO: throw an error when there are no more warning generated + logUnknownSymbol(link, symbol); + return {label, url: '#'}; + } + subSymbol = symbol; + symbol = currentSymbol; + } + + return {label, url: getLinkToModule(moduleName, symbol, subSymbol)}; }