docs(docs-infra): Throw error at build time for invalid links (#59162)

PR Close #59162
This commit is contained in:
Matthieu Riegler 2025-03-04 19:14:08 +01:00 committed by Miles Malerba
parent 6dd8cce155
commit 286e4da52b
5 changed files with 47 additions and 20 deletions

View file

@ -38,8 +38,6 @@ 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`,
);
export function unknownSymbolMessage(link: string, symbol: string): string {
return `WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`;
}

View file

@ -34,6 +34,13 @@ describe('markdown to html', () => {
const symbols = new Map<string, string>([
['AfterRenderPhase', 'core'],
['afterRender', 'core'],
['EmbeddedViewRef', 'core'],
['ChangeDetectionStrategy', 'core'],
['ChangeDetectorRef', 'core'],
['withNoHttpTransferCache', 'platform-browser'],
['withHttpTransferCacheOptions', 'platform-browser'],
['withI18nSupport', 'platform-browser'],
['withEventReplay', 'platform-browser'],
]);
setSymbols(symbols);
for (const entry of entryJson.entries) {

View file

@ -12,6 +12,7 @@ import {getRenderable} from '../processing';
import {DocEntryRenderable} from '../entities/renderables';
import {initHighlighter} from '../shiki/shiki';
import {configureMarkedGlobally} from '../marked/configuration';
import {setSymbols} from '../symbol-context';
// 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.
@ -28,6 +29,19 @@ describe('renderable', () => {
encoding: 'utf-8',
});
const entryJson = JSON.parse(entryContent) as any;
const symbols = new Map<string, string>([
['AfterRenderPhase', 'core'],
['afterRender', 'core'],
['EmbeddedViewRef', 'core'],
['ChangeDetectionStrategy', 'core'],
['ChangeDetectorRef', 'core'],
['withNoHttpTransferCache', 'platform-browser'],
['withHttpTransferCacheOptions', 'platform-browser'],
['withI18nSupport', 'platform-browser'],
['withEventReplay', 'platform-browser'],
]);
setSymbols(symbols);
for (const entry of entryJson.entries) {
const renderableJson = getRenderable(entry, '@angular/fakeentry') as DocEntryRenderable;
entries.set(entry['name'], renderableJson);

View file

@ -70,10 +70,6 @@ describe('jsdoc transforms', () => {
name: 'see',
comment: '{@link /cli/build ng build}',
},
{
name: 'see',
comment: '{@link cli/build ng build}',
},
{
name: 'see',
comment: '{@link /ecosystem/rxjs-interop/output-interop Output Interop}',
@ -135,16 +131,24 @@ describe('jsdoc transforms', () => {
url: '/cli/build',
});
// TODO: in the future when all links are valid we would throw an error instead when not starting with a slash
// Links should be absolute within adev (to support both next & stable site)
expect(entry.additionalLinks[11]).toEqual({
label: 'ng build',
url: 'cli/build',
});
expect(entry.additionalLinks[12]).toEqual({
label: 'Output Interop',
url: '/ecosystem/rxjs-interop/output-interop',
});
});
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/);
});
});

View file

@ -31,7 +31,7 @@ import {
import {getLinkToModule} from './url-transforms';
import {addApiLinksToHtml} from './code-transforms';
import {getCurrentSymbol, getModuleName, logUnknownSymbol} from '../symbol-context';
import {getCurrentSymbol, getModuleName, unknownSymbolMessage} from '../symbol-context';
export const JS_DOC_REMARKS_TAG = 'remarks';
export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes';
@ -186,6 +186,12 @@ function parseAtLink(link: string): {label: string; url: string} {
if (rawSymbol.startsWith('#')) {
rawSymbol = rawSymbol.substring(1);
} else if (rawSymbol.includes('/')) {
if (!rawSymbol.startsWith('/') && !rawSymbol.startsWith('http')) {
throw Error(
`Forbidden relative link: ${link}. Links should be absolute and start with a slash`,
);
}
return {
url: rawSymbol,
label: description ?? rawSymbol.split('/').pop()!,
@ -204,11 +210,9 @@ function parseAtLink(link: string): {label: string; url: string} {
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: '#'};
throw unknownSymbolMessage(link, symbol);
}
subSymbol = symbol;
symbol = currentSymbol;
}